文章目录

  • 第1章 数据结构和算法的概述
    • 数据结构和算法的关系
    • 线性结构和非线性结构
      • 线性结构
      • 非线性结构
  • 第2章 稀疏数组和队列
    • 稀疏数组
      • 案例引入
      • 稀疏数组的基本介绍
      • 应用实例
    • 队列
      • 队列介绍
      • 数组模拟队列
      • 数组模拟环形队列
  • 第3章 链表
    • 链表(LinkedList)介绍
    • 单链表的应用实例
    • 单链表的面试题
    • 双向链表应用实例
      • 双向链表的相关操作分析和实现
    • 单向环形链表应用场景
    • 单向环形链表介绍
    • Joseph问题
  • 第4章 栈
    • 栈的一个实际需求
    • 栈的介绍
    • 栈的应用场景
    • 栈的快速入门
      • 数组模拟栈
      • 链表模拟栈
    • 栈实现综合计算器(中缀表达式)
      • 前缀、中缀、后缀表达式
      • 使用栈来实现综合计算器
    • 逆波兰计算器
    • 中缀表达式转后缀表达式
      • 具体步骤
      • 举例说明
      • 代码实现(中缀转后缀与逆波兰综合版):
  • 第5章 递归
    • 递归应用场景
    • 递归的概念
    • 递归调用机制
    • 递归能解决什么样的问题
    • 递归需要遵守的重要规则
    • 递归-迷宫问题
      • 背景介绍:
      • 对迷宫问题的讨论
    • 递归-八皇后问题(回溯算法)
      • 回溯算法介绍
    • 回溯VS递归
      • 八皇后问题介绍
      • 八皇后问题思路分析
      • 八皇后问题代码实现
  • 第6章 排序算法
    • 排序算法的介绍
    • 排序算法的分类
    • 算法的时间复杂度
      • 度量一个程序(算法)执行时间的两种方法
      • 时间频度
      • 时间复杂度
      • 常见的时间复杂度
      • 平均时间复杂度和最坏时间复杂度
    • 算法的空间复杂度简介
    • 冒泡排序
      • 基本思想
      • 过程图解
      • 应用实例
    • 选择排序
      • 基本思想
      • 过程图解
      • 应用实例
    • 插入排序
      • 基本思想
      • 过程图解
      • 应用实例
    • 希尔排序
      • 简单插入排序存在的问题
      • 希尔排序法介绍
      • 希尔排序的基本思想
      • 过程图解
      • 应用实例
    • 快速排序
      • 基本思想
      • 过程图解
      • 关于快速排序的几个问题
      • 应用实例
    • 归并排序
      • 基本思想
      • 过程图解
      • 应用实例
    • 基数排序
      • 基数排序介绍
      • 基数排序的基本思想
      • 过程图解
      • 基数排序的说明
      • 应用实例
    • 常用排序算法总结和对比
  • 第7章 查找算法
    • 查找算法介绍
    • 线性查找算法
    • 二分查找算法
      • 思路过程
      • 二分查找的代码:
    • 插值查找算法
      • 原理介绍
      • 注意事项
      • 插值查找代码
    • 斐波拉契(黄金分割法)查找算法
      • 基本介绍
      • 查找原理
      • 斐波拉契查找的代码
  • 第8章 哈希表
    • 哈希表(散列)-Google上机题
    • 哈希表的基本介绍
      • 什么是哈希表?
    • google公司的一个上机题:

第1章 数据结构和算法的概述

整个文章主要来源于尚硅谷韩顺平数据结构与算法以及加上我查阅资料后加上自己的理解编写而成,若发现有 错误的地方,欢迎指正!

全文采用typora编辑而成,篇幅较大,此处为上部分,下部分

数据结构和算法的关系

  1. 数据data结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构.学好数据结构可以编写出更加漂亮,更加有效率的代码。
  2. 要学习好数据结构就要多多考虑如何将生活中遇到的问题,用程序去实现解决.
  3. 程序=数据结构+算法
  4. 数据结构是算法的基础,换言之,想要学好算法,需要把数据结构学好。

线性结构和非线性结构

数据结构包括:线性结构非线性结构

线性结构

  1. 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系。
  2. 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的
  3. 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。
  4. 线性结构常见的有:数组队列链表

非线性结构

非线性结构包括:二维数组,多维数组,广义表,结构,结构

第2章 稀疏数组和队列

稀疏数组

案例引入

  • 编写的五子棋程序中,有存盘退出和续上盘的功能。

  • 因为该二维数组的很多值是默认值0,从而记录了许多没有意义的数据.->稀疏数组

稀疏数组的基本介绍

当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。

稀疏数组的处理方法:

1)记录数组一共有几行几列,有多少个不同的值

2)把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模

实例说明:

应用实例

  • 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等)

  • 把稀疏数组存盘,并且可以从新恢复原来的二维数组数

  • 思路分析:

二维数组 转 稀疏数组的思路

  1. 遍历原始的二维数组,得到有效数据的个数 sum
  2. 根据sum 就可以创建 稀疏数组 sparseArr int[sum + 1] [3]
  3. 将二维数组的有效数据数据存入到 稀疏数组

稀疏数组转原始的二维数组的思路

  1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组。
  2. 在读取稀疏数组后几行的数据,并赋给 原始的二维数组 即可。

代码实现

package cn.ysk.exercise;import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;public class SparseArray {public static void main(String[] args) throws IOException {//创建一个原始的二维数组 11*11//0表示没有棋子,1表示黑色棋子,2表示蓝色棋子int[][] chessArray1 = new int[11][11];chessArray1[1][2] = 1;chessArray1[2][3] = 2;System.out.println("原始数组为:");for (int i = 0; i < chessArray1.length; i++) {for(int j = 0;j < chessArray1[i].length; j++){System.out.print(chessArray1[i][j] + "\t");}System.out.println();}/* 二维数组转成稀疏数组的思路:1.遍历原始的二维数组,得到有效的个数sum2.根据sum创建稀疏数组 sparseArr int[sum+1][3]3.将二维数组的有效数据存入到稀疏数组*/System.out.println("压缩成稀疏数组是:");int sum = 0; //计数变量,统计原矩阵中非零元素的个数for (int i = 0; i < chessArray1.length; i++) {for(int j = 0;j < chessArray1[i].length; j++){if(chessArray1[i][j] != 0){sum++;}}}int[][] sparseArr = new int[sum+1][3];//给稀疏数组赋值sparseArr[0][0] = 11;sparseArr[0][1] = 11;sparseArr[0][2] = sum;int count = 0; //用于记录是第几个非零数据for(int i = 0;i < chessArray1.length; i++){for (int j = 0; j < chessArray1[i].length; j++) {if(chessArray1[i][j] != 0){count++;sparseArr[count][0] = i;sparseArr[count][1] = j;sparseArr[count][2] = chessArray1[i][j];}}}//遍历稀疏数组for (int i = 0; i < sparseArr.length; i++) {for(int j = 0;j < sparseArr[i].length;j++){System.out.print(sparseArr[i][j] + "\t");}System.out.println();}/*将稀疏数组转成二维数组思路:1.先读取稀疏数组的第一行,根据第一行创建原始的二维数组。2.再读取稀疏数组的后几行的数组,并赋值给原始二维数组。*/int[][] chessArray2 = new int[sparseArr[0][0]][sparseArr[0][1]];for (int i = 1; i < sparseArr.length ; i++) {chessArray2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];}//恢复后的二维数组System.out.println("恢复后的二维数组是:");for (int i = 0; i < chessArray2.length; i++) {for (int j = 0; j < chessArray2[i].length; j++) {System.out.print(chessArray2[i][j] + "\t");}System.out.println();}//将数组以txt文件的形式保存到本地磁盘以及取出还原String location = "E:\\尚硅谷Java数据结构与java算法\\课后习题\\sparse.txt";File file = new File(location);if(!file.exists()){file.createNewFile(); //文件按不存在则创建}//创建字符流写入对象FileWriter fileWriter = new FileWriter(location);for (int i = 0; i < sparseArr.length; i++) {for (int j = 0; j < sparseArr[i].length; j++) {//写入每个字符fileWriter.write(sparseArr[i][j]);}}//关闭写入流fileWriter.close();System.out.println("写入完毕");//创建新的数组,来接收读取的数据int [][] sparseArr2 = new int[sum+1][3];FileReader fileReader = new FileReader(location);for (int i = 0; i < sparseArr2.length; i++) {for (int j = 0; j < sparseArr[i].length; j++) {//把读取的数据设置到数组中sparseArr2[i][j] = fileReader.read();}}//关闭读取流fileReader.close();System.out.println("读取的数组:");for (int i = 0; i < sparseArr2.length; i++) {for (int j = 0; j < sparseArr2[i].length; j++) {System.out.print(sparseArr2[i][j] + "\t");}System.out.println();}}
}

队列

队列介绍

  • 队列是一个有序列表,可以用数组或是链表来实现。
  • 仅允许在表的一端进行插入,在表的另一端进行删除。把进行插入的一端称作队尾,进行删除的一端称作队首或队头。
  • 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出

数组模拟队列

  • 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图,其中maxSize是该队列的最大容量。

  • 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量front及rear分别记录队列前后端的下标,front会随着数据输出而改变,而rear则是随着数据输入而改变,如图所示:

在这里,front是指向队列头的前一个位置,指向队列的尾部,最后一个元素的位置

  • 当我们将数据存入队列时称为”addQueue”,addQueue的处理需要有两个步骤:思路分析

    1. 将尾指针往后移:rear+1,当front==rear【空】
    2. 若尾指针rear小于队列的最大下标maxSize-1,则将数据存入rear所指的数组元素中,否则无法存入数据。
    3. rear==maxSize-1,队列已满。

代码实现

package cn.ysk.queue;import java.util.Scanner;public class ArrayQueueDemo {public static void main(String[] args) {ArrayQueue arrayQueue = new ArrayQueue(3);Scanner scanner = new Scanner(System.in);char key;//接收用户输入boolean loop = true;while (loop){System.out.println("s(show):显示队列!");System.out.println("e(exit):退出程序");System.out.println("a(add):添加数据到队列");System.out.println("g(get):从队列取出数据");System.out.println("h(head):查看队列头的数据");System.out.println("请输入您的选择:");key = scanner.next().charAt(0);switch (key){case 's':arrayQueue.showQueue();System.out.println();break;case 'a':System.out.println("请输入要添加的数据:");int value = scanner.nextInt();arrayQueue.addQueue(value);break;case 'g':try {int res = arrayQueue.getQueue();System.out.println("去除的数据是:" + res);} catch (Exception e) {System.out.println(e.getMessage());}break;case'h': //查看队头数据try {int res =arrayQueue.showHead();System.out.println("队头的数据是:" + res);} catch (Exception e) {e.printStackTrace();}break;case 'e': //退出scanner.close();loop = false;break;default:System.out.println("您输入的数据有误,请重新输入:");break;}}}
}
class ArrayQueue{private int maxSize; //数组的最大容量private int front; //队头private int rear; //队尾private int[] arr;//用于存放数据,模拟队列//创建队列构造器public ArrayQueue(int arrMaxSize){maxSize = arrMaxSize;arr = new int[maxSize];front = -1;  //指向队列头部,front是指向队列头的前一个位置rear = -1;  //指向队列的尾部,最后一个元素的位置。}//判断队列是否已满public boolean isFull(){return rear == maxSize -1;}//判断队列是否为空public boolean isEmpty(){return rear == front;}//添加数据public void addQueue(int n){if(isFull()){System.out.println("队列满,不能加入数据!");return;}rear++; //让rear后移arr[rear] = n;}//出队public int getQueue(){if(isEmpty()){throw new RuntimeException("队列空,不能取数据!");}front++; //front后移return arr[front];}//显示队列的所有数据public void showQueue(){if(isEmpty()){System.out.println("队列空,不能取数据!");return;}for (int i = 0; i < arr.length; i++) {System.out.printf("arr[%d]=%d\t",i,arr[i]);}}//显示队列的头数据public int showHead(){if(isEmpty()){throw new RuntimeException("队列空,不能取数据!");}return arr[front+1];}
}
  • 问题分析并优化

    1. 目前数组使用一次就不能用,没有达到复用的效果(“一次性队列”)
    2. 将这个数组使用算法,改进成一个环形的队列

数组模拟环形队列

对前面的数组模拟队列的优化,充分利用数组,减少空间的浪费。因此将数组看做是一个环形的。(通过取模的方式来实现即可)

分析说明

模拟循环队列的思路:

  1. front 变量的含义做一个调整: front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素
    front 的初始值 = 0(注意这里和普通队列的区别)

  2. rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定.
    rear 的初始值 = 0(注意这里和普通队列的区别)

    注意这里为什么要指向最后一个元素的后一个位置:

    之前判断队空是front=rear时就判断为空,如果rear还是指向最后一个元素就会造成front=rear既可以表示队空也能表示队满,所以在循环队列中rear始终指向最后一个元素的后一个位置(即拿出一个位置作为约定,谁都不要占用),这样做,循环队列最多只能存放MAXSIZE-1个数据元素。

  3. 尾索引的下一个为头索引时表示队列满(rear+1)%maxSize==front(队列满)

  4. 对队列为空的条件, rear == front( 队列空)

  5. 当我们这样分析, 队列中有效数据的个数 :(rear + maxSize - front) % maxSize // rear = 1 front = 0

  6. 我们就可以在原来的队列上修改得到,一个环形队列

代码实现:

package cn.ysk.queue;import java.util.Scanner;public class CircleArrayQueueDemo {public static void main(String[] args) {CircleArray arrayQueue = new CircleArray(4);//说明设置4,其队列的有效数据最大是3Scanner scanner = new Scanner(System.in);char key;//接收用户输入boolean loop = true;while (loop){System.out.println("s(show):显示队列");System.out.println("e(exit):退出程序");System.out.println("a(add):添加数据到队列");System.out.println("g(get):从队列取出数据");System.out.println("h(head):查看队列头的数据");System.out.println("请输入您的选择:");key = scanner.next().charAt(0);switch (key){case 's':arrayQueue.showQueue();System.out.println();break;case 'a':System.out.println("请输入要添加的数据:");int value = scanner.nextInt();arrayQueue.addQueue(value);break;case 'g':try {int res = arrayQueue.getQueue();System.out.println("去除的数据为:" + res);} catch (Exception e) {System.out.println(e.getMessage());}break;case'h': //查看队头数据try {int res =arrayQueue.showHead();System.out.println("队头的数据是:" + res);} catch (Exception e) {e.printStackTrace();}break;case 'e': //退出scanner.close();loop = false;break;default:System.out.println("您输入的数据有误,请重新输入:");break;}}}}
class CircleArray{private int maxSize; //数组的最大容量//front变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素private int front; //初始值0//rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置.因为希望空出一个空间做为约定private int rear; //初始值0private int[] arr;//用于存放数据,模拟队列//创建队列构造器public CircleArray(int arrMaxSize){maxSize = arrMaxSize;arr = new int[maxSize];  //初始值都为零,不在为其赋值}//判断队列是否已满public boolean isFull(){return (rear + 1) % maxSize == front;}//判断队列是否为空public boolean isEmpty(){return rear == front;}//添加数据public void addQueue(int n){if(isFull()){System.out.println("队列满,不能加入数据!");return;}/*这里和之前非循环队列不同,前者的rear是指向最后一个有效的数据元素,而在这里,rear指向的是最后一个有效元素的下一个元素(即无效的数据)。所以前者要先+1再赋值,而这里是先赋值rear再向后移*/arr[rear] = n;rear = (rear + 1) % maxSize;}//出队public int getQueue(){if(isEmpty()){throw new RuntimeException("队列空,不能取数据!");}//这里需要分析出front是指向队列的第一个元素// 1.先把front对应的值保留到一个临时变量// 2.将front后移,考虑取模// 3.将临时保存的变量返回int temp = arr[front];front = (front + 1) % maxSize;return temp;}//显示队列的所有数据public void showQueue(){if(isEmpty()){System.out.println("队列空,不能取数据!");return;}for (int i = front; i < front + size(); i++) {System.out.printf("arr[%d]=%d\t",i%maxSize,arr[i%maxSize]);}}//显示队列的头数据public int showHead(){if(isEmpty()){throw new RuntimeException("队列空,不能取数据!");}return arr[front]; //这里front指向的是第一个有效的数据元素,所以不再+1}//计算当前队列的有效数据个数public int size(){//加maxSize是为了防止出现负数return (rear + maxSize - front) % maxSize;}
}

第3章 链表

链表(LinkedList)介绍

链表是有序的列表,但是它在内存中是存储如下:

  • 链表是以节点的方式来存储,是链式存储
  • 每个节点包含data域,next域:指向下一个节点.
  • 如图:发现链表的各个节点不一定是连续存储.
  • 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
  • 链表的优点:空间没有限制,插入删除很快。缺点:存取速度很慢。

单链表(带头结点)逻辑结构示意图如下:

单链表的应用实例

使用带head头的单向链表实现–水浒英雄排行榜管理完成对英雄人物的增删改查操作:

  1. 第一种方法在添加英雄时,直接添加到链表的尾部

  2. 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)

    思路的分析示意图:

  1. 修改节点功能

    (1)先找到该节点,通过遍历,(2)temp.name=newHeroNode.name;temp.nickname=newHeroNode.nickname

  2. 删除节点

    思路分析示意图:

5.完整代码演示

package cn.ysk.linkedlist;import java.util.Stack;public class SingleLinkedListDemo1 {public static void main(String[] args) {HeroNode hero1 = new HeroNode(1,"李逵","黑旋风");HeroNode hero2 = new HeroNode(2,"宋江","及时雨");HeroNode hero3 = new HeroNode(3,"吴用","智多星");HeroNode hero4 = new HeroNode(4,"林冲","豹子头");       SingleLinkedList singleLinkedList = new SingleLinkedList();singleLinkedList.add2(hero1);singleLinkedList.add2(hero2);singleLinkedList.add2(hero3);singleLinkedList.showList(singleLinkedList.getHead());
//        singleLinkedList.addByOrder(hero1);
//        singleLinkedList.addByOrder(hero3);
//        singleLinkedList.addByOrder(hero2);
//        singleLinkedList.addByOrder(hero4);}
}//SingleLinkedList管理我们的英雄
class SingleLinkedList{//定义头节点,数据域为空private HeroNode head = new HeroNode(0,"","");public HeroNode getHead() {return head;}public static void showList(HeroNode head){HeroNode p = head.next;if(p == null){return;}while (p != null){System.out.println(p);p = p.next;}}//添加节点到单向链表// 思路,当不考虑编号顺序时// 1.找到当前链表的最后节点// 2.将最后这个节点的next指向新的节点/*** 添加节点(尾插法)* @param heroNode*/public void add(HeroNode heroNode){//head节点不能动,定义tail保存headHeroNode tail = head;//要一直遍历,直至找到链表的尾部while (true){if(tail.next == null ){break;  //找到链表的尾部之后跳出循环}//未找到链表的尾部,则向下移动一个节点tail = tail.next;}//到达尾部之后,最后节点的next指向新的节点tail.next = heroNode;}//头插法创建链表public void add2(HeroNode heroNode){heroNode.next = head.next;head.next = heroNode;}//第二种方式在添加英雄时,根据排名将英雄插入到指定位置/*** 按照排名添加节点* @param heroNode*/public void addByOrder(HeroNode heroNode){//因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置//因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了HeroNode temp = head;boolean flag = false;   //标志添加的编号是否存在while(true){if(temp.next == null){  //已在链表的最后,不管找到未找到都要break,可能要添加的节点位于最后面break;}//找到指定位置if(temp.next.no > heroNode.no ){break; //找到位置,跳出循环}else if(temp.next.no == heroNode.no){flag = true;break; //证明已存在该编号}temp = temp.next; //temp往后移动}if(flag){System.out.printf("%d编号已存在,不能加入\n",heroNode.no);}else{heroNode.next = temp.next;temp.next = heroNode;}}//遍历链表
//    public void showSingleLinkedList(){//        //判断链表是否为空
//        if(head.next == null){//            System.out.println("链表为空!");
//            return;
//        }
//        HeroNode temp = head.next;
//        while(true){//            //遍历已达到链表的最后
//            if(temp == null){//                break;
//            }
//            System.out.println(temp);
//            temp = temp.next; //将temp往后移动
//        }
//    }/*** @version* 修改人物的相关信息* @param newHeroNode*/public void update(HeroNode newHeroNode){if(head.next == null){System.out.println("链表为空!");return;}HeroNode temp = head.next;boolean flag = false; //标记是否找到该节点while(true){if(temp == null){//到达链表尾端break;}if(temp.no == newHeroNode.no){flag = true;break;}temp = temp.next;}if(flag){temp.nickName = newHeroNode.nickName;temp.name = newHeroNode.name;System.out.println("修改成功!");}else{System.out.printf("没有找到编号为%d的节点,修改失败\n",newHeroNode.no);}}/*** 删除某个节点* @param heroNode*/public void del(HeroNode heroNode){//判断是否空if(head.next==null){System.out.println("链表为空~");return;}HeroNode temp = head;   //这里没有定义为head.next是为了方便找到待删除节点的前一个节点boolean flag = false;while (true){if(temp.next == null){break;}if(temp.next.no == heroNode.no){ //注意这里是temp节点的下一个节点是要删除的节点flag = true;break;}temp = temp.next;}if(flag){temp.next = temp.next.next;System.out.println("删除节点" + heroNode.no + "成功!");}else{System.out.printf("没有找到编号为%d的节点,删除失败\n",heroNode.no);}}
}class HeroNode{public int no; //编号public String name; //姓名public String nickName; //绰号public HeroNode next;   //指向下一个节点//构造器public HeroNode(int no,String name,String nickName){this.no = no;this.name = name;this.nickName = nickName;}//重写toString@Overridepublic String toString() {return "HeroNode{" +"no=" + no +", name='" + name + '\'' +", nickName='" + nickName + '\'' +'}';}
}

单链表的面试题

单链表的常见面试题有如下:

  1. 求单链表中有效节点的个数

    代码实现:

     /***  计算链表的有效节点个数* @param head* @return count*/public static int getCount(HeroNode head){HeroNode cur = head.next;int count = 0 ;if(cur == null){return 0;      //链表为空,返回值为0}while(cur != null){count++;cur = cur.next;}return count;}
    
  2. 查找单链表中的倒数第k个结点【新浪面试题】

    代码实现:

    //思路//1.编写一个方法,接收head节点,同时接收一个index
    //2.index表示是倒数第index个节点
    //3.先把链表从头到尾遍历,得到链表的总的长度getLength
    //4.得到size后,我们从链表的第一个开始遍历(size-index)个,就可以得到
    //5.如果找到了,则返回该节点,否则返回null
    public static HeroNode findLastIndexNode(HeroNode head,int index){if(head.next == null){return null; //链表为空}int count = getCount(head);if(index <=0 || index > count){return null;}HeroNode cur = head.next;for (int i = 0; i < count-index; i++) {cur = cur.next;}return cur;}
    
  3. 单链表的反转【腾讯面试题,有点难度】

    思路分析:

    • 先定义一个节点 reverseHead
    • 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端.(类似于头插法)
    • 原来的链表的head.next = reverseHead.next

代码实现:

 public static void reverseList(HeroNode head){if(head.next == null || head.next.next == null){//如果单链表为空,或者单链表只有一个节点,无需反转return;}HeroNode reverseHead = new HeroNode(0,"",""); //为反转链表创建头节点HeroNode cur = head.next;HeroNode curNext = null;//指向当前节点[cur]的下一个节点//遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端while (cur != null){curNext = cur.next; //保存当前节点的下一个节点cur.next = reverseHead.next;  //这两步相当于头插法reverseHead.next = cur;cur = curNext;  //让cur向后移动}//将head.next指向reverseHead.next,实现单链表的反转head.next = reverseHead.next;}
  1. 从尾到头打印单链表【百度,要求方式1:反向遍历。方式2:Stack栈】

可以利用这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果

public static void reversePrint(HeroNode head){if(head.next == null){return; //空链表无法打印}//创建栈Stack<HeroNode> stack = new Stack<>();HeroNode cur = head.next;while(cur != null){stack.push(cur);cur = cur.next;}while (stack.size() > 0){System.out.println(stack.pop());}}

5.合并两个有序的单链表,合并之后的链表依然有序

思路:

  • 若一个链表为空,则直接返另一链表,两者都为空,则返回空。
  • 首先通过第一个有效节点的值确定头节点的位置。
  • 接着循环遍历,将较小的值添加到新链表中。
  • 遍历完成之后,将未遍历完的链表添加到新链表中即可。
public static HeroNode mergeLinkedList(HeroNode head1,HeroNode head2){if (head1.next == null && head2.next == null) {// 如果两个链表都为空 return null;return null;}if (head1.next == null) {return head2;}if (head2.next == null) {return head1;}HeroNode head;HeroNode tail;HeroNode p = head1.next;HeroNode q = head2.next; //p,q分别指向第一个节点// 比较第一个节点的大小 确定头结点的位置if (p.no < q.no) {head = head1;p = p.next;} else {head = head2;q = q.next;}tail = head.next;  //tail指向已排好序的最后一个节点while (p != null && q != null) {if (p.no < q.no) {tail.next = p;p = p.next;} else {tail.next = q;q = q.next;}tail = tail.next;}// 合并剩余的元素if (p != null) {// 说明链表2遍历完了,是空的tail.next = p;}if (head2 != null) {// 说明链表1遍历完了,是空的tail.next = q;}return head;}

双向链表应用实例

双向链表的相关操作分析和实现

使用带head头的双向链表实现–水浒英雄排行榜

管理单项链表的缺点分析:

  • 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
  • 单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点

双向链表的创建,删除,修改,遍历,插入操作:

  • 创建:

默认添加到双向链表的最后

  1. 先找到双向链表的最后这个节点
  2. temp.next=newHeroNode
  3. newHeroNode.pre=temp;

代码实现:

public void add(HeroNode heroNode){//head节点不能动,定义tail保存headHeroNode tail = head;//要一直遍历,直至找到链表的尾部while (true){if(tail.next == null ){break;  //找到链表的尾部之后跳出循环}//未找到链表的尾部,则向下移动一个节点tail = tail.next;}//当退出while循环时,temp就指向了链表的最后//形成一个双向链表tail.next = heroNode;heroNode.pre = tail;}

按顺序添加:

此处和单链表的实现思路一样,不同之处在于找到要插入的地方之后,进行的插入操作(后面详细介绍)不同!

注意:此处的插入操作为后插操作(将新结点插入到已知节点的后方),在后面介绍的是前插操作,原理相同,灵活变通即可。

代码实现:

public void addByOrder(HeroNode heroNode){HeroNode temp = head;boolean flag = false;   //标志添加的编号是否存在while(true){if(temp.next == null){  //已在链表的最后,不管找到未找到都要break,可能要添加的节点位于最后面break;}//找到指定位置if(temp.next.no > heroNode.no ){  //后面的数字比他大就插入break; //找到位置,跳出循环}else if(temp.next.no == heroNode.no){flag = true;break; //证明已存在该编号}temp = temp.next; //temp往后移动}if(flag){System.out.printf("%d编号已存在,不能加入\n",heroNode.no);}else{heroNode.next = temp.next;if(temp.next != null){ //已经到最后一个节点temp.next.pre = heroNode;}heroNode.pre = temp;temp.next = heroNode;}}
  • 插入

带头节点的前插操作示意图:

以上图的双向链表为例,可以这样理解:要插入节点s的前驱要指向a,a节点的后驱要指向s,s的后驱要指向b,b的前驱要指向s。

用代码实现就是图中的四句代码,这是前插,后插只需要根据思路灵活变通即可。

注意:算法中的操作步骤不是唯一的,但是某些操作的顺序不能颠倒,操作步骤1必须在4之前完成,否则p节点的指向前驱节点的指针就丢失了。另外,一定一定要注意空指针异常!!!例如上面的“按顺序插入操作”中在最后一个节点的后方插入时就加入了一个限制条件解决了这个问题。

  • 删除:

  1. 找到要删除的节点p
  2. 将p的前驱的后继指向p的后继,即p->prior->next = p->next;
  3. 将p的后继的前驱指向p的前驱,即p->next->prior = p->prior;

代码实现:

//对于双向链表,我们可以直接找到要删除的这个节点//找到后,删除自身即可public void del(int no){//判断是否空if(head.next==null){System.out.println("链表为空,无法删除!");return;}HeroNode temp = head.next; //不需要找到前一个结点,直接定义为head.nextboolean flag = false;while (true){if(temp == null){break;}if(temp.no == no){ //注意这里是temp节点的下一个节点是要删除的节点flag = true;break;}temp = temp.next;}//当退出while循环时,temp就指向了链表的最后//形成一个双向链表if(flag){temp.pre.next = temp.next;  //修改待删除结点的前驱结点的后继指针//如果是最后一个节点,就不需要执行下面这句话,否则出现空指针异常if(temp.next != null){temp.next.pre = temp.pre;   //修改待删除节点的后继结点的前驱指针}System.out.println("删除节点" + no + "成功!");}else{System.out.printf("没有找到编号为%d的节点,删除失败\n",no);}}
  • 修改节点

思路和之前单链表的一样,不再赘述

代码实现:

public void update(HeroNode newHeroNode){if(head.next == null){System.out.println("链表为空!");return;}HeroNode temp = head.next;boolean flag = false; //标记是否找到该节点while(true){if(temp == null){//到达链表尾端break;}if(temp.no == newHeroNode.no){flag = true;break;}temp = temp.next;}if(flag){temp.nickName = newHeroNode.nickName;temp.name = newHeroNode.name;System.out.println("修改成功!");}else{System.out.printf("没有找到编号为%d的节点,修改失败\n",newHeroNode.no);}}
  • 遍历

也和单链表一样,不再赘述

代码实现:

 public static void showList(HeroNode head){HeroNode p = head.next;if(p == null){return;}while (p != null){System.out.println(p);p = p.next;}}

单向环形链表应用场景

Josephu(约瑟夫、约瑟夫环)问题

Josephu问题为:设编号为1,2,…n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。

提示:用一个不带头结点的循环链表来处理Josephu问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。(要注意,从1开始,若m=2,出列的是2,不是3!)

单向环形链表介绍

Joseph问题

主要思路:用循环链表模拟n个人围坐在一起,删除某个节点代表某个人出列。即创建链表,删除节点。

详细介绍:

创建链表(带头节点)过程比较常规,不再介绍。

删除节点详细介绍:

  • 循环链表完成创建之后,让p指针指向链表的第一个节点,q是p的前驱节点,指向tail。p和q指针始终保持一前一后的关系。

  • 设置报数变量i,初始值为1。进入循环,若报数变量i不等于m,则用q保存p的位置,p移动到下一个节点。此时相当于p和q都往后移动了一个单位。报数变量i+1。

  • 这样循环往复,直至报数变量i与m相等时,证明已找到要删除的位置

  • 循环一直进行下去,当p和q相等时,表示只剩下一个节点,循环结束。可得出所有的出列序列。

以上的思路过程是我根据B站上懒猫老师解约瑟夫环总结的思路过程,若看完之后还是不太理解,可以直接去看老师的视频,讲的很清晰。虽然是c语言版的,题目稍微有点差别,不过影响不大,理解原理之后灵活变通即可!

完整代码:

package cn.ysk.linkedlist;public class Joseph {public static void main(String[] args) {CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();Person head = circleSingleLinkedList.add(5);circleSingleLinkedList.showList(head);circleSingleLinkedList.delNode(head, 2);}
}
class CircleSingleLinkedList{private Person head = new Person();//添加人,构建循环链表public Person add(int count){ //创建的是带头结点的单循环链表if(count < 1){System.out.println("人物数量有误!");return null;}Person tail = head;for (int i = 1; i <= count; i++) {Person p = new Person(i);tail.next = p; //修改尾节点指针域tail = p;   //修改尾指针}tail.next = head.next;  //链表有数据的部分首尾相连形成一个环。return head;}public void showList(Person head){if(head == null){System.out.println("为空!");return;}Person cur = head.next;while (true){if(cur.next != head.next){System.out.println(cur);cur = cur.next;}else{break;}}System.out.println(cur);}/**** @param head 头节点* @param m 间隔的人数*/public void delNode(Person head,int m){Person p = head;Person q = head.next; //两个指针始终保持一前一后的关系,为后面的删除做准备int i = 1; //定义报数变量if(m <= 0){System.out.println("m值非法!");return;}while (p != q){ //两者相等就证明只剩下一个节点,结束循环if(i == m){p.next = q.next; //删除节点System.out.println("第" + q.num + "号人出列");q = p.next;  //将p移动到下一个有效节点当中i = 1; //报数变量重新报数}else{p = p.next;q = q.next; //p,q向后移动,报数变量加1i++;}}System.out.println("第" + q.num + "号人出列");}
}
class Person {int num;Person next;public Person(){}public Person(int num){this.num = num;}@Overridepublic String toString() {return "Person{" +"num=" + num +'}';}
}

第4章 栈

栈和队列是两种重要的线性结构。从数据结构角度看,栈和队列也是线性表,它们是操作受限的线性表;从数据类型角度看,栈和队列是不同于线性表的两类重要的抽象数数据类型。

栈的一个实际需求

请输入一个表达式

计算式:[722-5+1-5+3-3] 点击计算【如下图】

请问: 计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式 7 * 2 * 2 - 5, 但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题。->

栈的介绍

  1. 栈的英文为(stack)
  2. 栈是一个先入后出(FILO-First In Last Out)的有序列表。
  3. 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
  4. 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
  5. 出栈(pop)和入栈(push)的概念(如图所示)


栈的应用场景

  1. 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
  2. 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
  3. 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
  4. 二叉树的遍历。
  5. 图形的深度优先(depth一first)搜索法。

栈的快速入门

与线性表类似,栈在计算机中也主要有两种基本的存储结构,即顺序存储结构和链式存储结构,分别可以用数组或链表来实现。

数组模拟栈

  1. 数组模拟栈的使用,由于栈是一种有序列表,当然可以使用数组的结构来储存栈的数据内容,下面我们就用数组模拟栈的出栈入栈等操作。
  2. 实现思路分析,并画出示意图:

思路过程:

  • 使用数组来模拟栈
  • 定义一个 top 来表示栈顶,初始化 为 -1
  • 入栈的操作,当有数据加入到栈时, top++; stack[top] = data;
  • 出栈的操作, int value = stack[top]; top–, return value(数组模拟的栈出栈后数据没有删除,若采用动态分配空间实现栈,则可以删除)

3.代码实现:

package cn.ysk.stack;import java.util.Scanner;public class ArrayStackDemo {public static void main(String[] args) {ArrayStack arrayStack = new ArrayStack(4);String key;boolean loop = true;Scanner sc = new Scanner(System.in);while (loop) {System.out.println("show:表示显示栈");System.out.println("exit:退出程序");System.out.println("push:表示添加数据到栈(入栈)");System.out.println("pop:表示从栈取出数据(出栈)");System.out.println("请输入你的选择:");key = sc.nextLine();switch (key){case "show":arrayStack.showStack();break;case "push":System.out.println("请输入一个数:");int value = sc.nextInt();arrayStack.push(value);break;case "pop":try {int res = arrayStack.pop();System.out.printf("出栈的数据是:%d\n",res);} catch (Exception e) {System.out.println(e.getMessage());}break;case "exit":sc.close();loop = false;break;}}System.out.println("程序退出!");}
}
class ArrayStack {private int maxSize;private int[] stack;private int top = -1;//构造器public ArrayStack(int maxSize) {this.maxSize = maxSize;stack = new int[this.maxSize];}//栈满public boolean isFull() {return top == maxSize-1;}//栈空public boolean isEmpty() {return top == -1;}//入栈pushpublic void push(int value) {if(isFull()){System.out.println("栈满!");return;}top++;stack[top] = value;}//出栈pop,将栈顶的数据返回public int pop() {if(isEmpty()){throw new RuntimeException("栈空!"); //运行时异常,可捕获也可不捕获}int value = stack[top];top--;return value;}//遍历栈,遍历时,从栈顶开始显示数据public void showStack() {if(isEmpty()){System.out.println("栈为空,无法遍历!");}for (int i = top; i >= 0; i--) {System.out.printf("stack[%d] = %d\n",i,stack[i]);}}}

链表模拟栈

采用链式存储结构实现的栈称为链栈。链栈通常使用单链表来实现,因此其结构与单链表的相同。由于栈的插入和删除操作仅限制在栈顶位置进行,所以采用单链表的表头指针作为栈顶指针。同时为了操作方便使用带头结点的单链表来实现链栈。

指针变量top用来存放单链表头节点的指针,而用头节点指向表的第一个结点,即链栈的栈顶数据元素。这样,确定链表的左端的为栈顶,右端为栈底。下图是链栈的存储结构示意图:

由于使用带头结点的链表,在对链栈操作的过程中,栈顶指针top始终指向头节点,当栈顶数据元素发生改变时,实际更改的是头节点的指针域。若top.next == null,则表示栈为空。与顺序栈不同,使用链栈时不用事先检查栈是否为满,只要系统有可用空间,链栈就不会溢出。

代码实现:

package cn.ysk.stack;import java.util.Scanner;public class LinkedListStackDemo {public static void main(String[] args) {LinkedListStack linkedListStack = new LinkedListStack();String key;boolean loop = true;Scanner sc = new Scanner(System.in);while (loop) {System.out.println("s(show):显示栈");System.out.println("e(exit):退出程序");System.out.println("p(push):表示添加数据到栈(入栈)");System.out.println("pp(pop):表示从栈取出数据(出栈)");System.out.println("请输入你的选择:");key = sc.nextLine();switch (key){case "s":linkedListStack.showStack();break;case "p":System.out.println("请输入一个数:");int value = sc.nextInt();Node node = new Node(value);linkedListStack.push(node);break;case "pp":try {Node res = linkedListStack.pop();System.out.println("出栈的节点是"+ res);} catch (Exception e) {System.out.println(e.getMessage()); //栈为空}break;case "exit":sc.close();loop = false;break;}}System.out.println("程序退出!");}
}
class LinkedListStack {private Node top = new Node(0);public Node getHead() {return top;}//栈空public boolean isEmpty() {return top.next == null;}//入栈操作public void push(Node node) {//在这里使用头插法插入更为方便,可较好的模拟“先入后出”的特性//另外,因为使用的是链表,不必考虑栈满的情况node.next = top.next;top.next = node;}public Node pop() {//判断链表是否为空if(isEmpty()){throw new RuntimeException("栈为空,无法出栈!");}//头节点不能动,借助辅助节点完成出栈Node cur = top.next;Node temp = cur;top.next = cur.next;return temp;}public void showStack() {if(isEmpty()) {System.out.println("栈为空,无法遍历!");return;}Node p = top.next;while (p!=null) {System.out.println(p);p = p.next;}}
}
class Node {public int value;  //存储的数据public Node next;   //下一个节点//构造器public Node(int value){this.value = value;}@Overridepublic String toString() {return "Node{" +"value=" + value +'}';}
}

栈实现综合计算器(中缀表达式)

前缀、中缀、后缀表达式

  • 前缀表达式(波兰表达式)
  1. 前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
  2. 举例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6
  • 中缀表达式
  1. 中缀表达式就是常见的运算表达式,如(3+4)×5-6
  2. 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)
  • 后缀表达式
  1. 后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
  2. 举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –

使用栈来实现综合计算器

思路分析:

代码实现:

package cn.ysk.example;public class Calculator {public static void main(String[] args) {String experssion  = "7*2*2-5+1-5+3-4"; //定义表达式//创建两个栈,树栈和符号栈ArrayStack numStack = new ArrayStack(10);ArrayStack operStack = new ArrayStack(10);int index = 0;//用于扫描int num1 = 0;int num2 = 0;int oper = 0;int res = 0;char ch; //用于保存每次扫描得到的charString keepNum = ""; //用于多位数的拼接while (true){//依次得到expression 中的每一个字符ch = experssion.substring(index,index+1).charAt(0);//如果发现扫描到是一个符号,  就分如下情况if(operStack.isOper(ch)){//如果是运算符//如果符号栈有运算符,就进行比较,如果当前的操作符的优先级小于或者等于栈中的操作符,if(!operStack.isEmpty()){if(operStack.priority(ch) <= operStack.priority(operStack.peek())){// 就需要从数栈中pop出两个数,再从符号栈中pop出一个符号,进行运算,将得到结果,入数栈,然后将当前的操作符入符号栈,num1 = numStack.pop();num2 = numStack.pop();oper = operStack.pop();res = numStack.cal(num1, num2, oper);//将运算出的结果入栈numStack.push(res);//将此时未入栈的操作符入栈operStack.push(ch);}else {// 如果当前的操作符的优先级大于栈中的操作符, 就直接入符号栈operStack.push(ch);}}else{//如果此时是运算符,并且operStack是空栈,那么直接入栈operStack.push(ch);}}else {//如果是数字//numStack.push(ch-48);将字符转为数字//分析思路/*1.当处理多位数时,不能发现是一个数就立即入栈,因为它可能是多位数2.在处理数,需要向expression的表达式的index后再看一位,如果是数就进行扫描,如果是符号才入栈3.因此我们需要定义一个字符串用于拼接*/keepNum+=ch;//如果ch是expression中的最后一位,则直接入栈if(index == experssion.length() -1){numStack.push(Integer.parseInt(keepNum));}else {//判断下一个字符是不是数字,如果是数字,就继续扫描,如果是运算符,则刚才得到的数字入栈//注意是看后一位,不是index++if(operStack.isOper(experssion.substring(index+1,index+2).charAt(0))){numStack.push(Integer.parseInt(keepNum));//!!这里一定要将keepNum清空keepNum = "";}}}index++;if(index >= experssion.length()){break;}}//当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号,并运行.while(true){if(operStack.isEmpty()){ //如果符号栈为空,则数栈中只剩下一个数字(结果)break;}num1 = numStack.pop();num2 = numStack.pop();oper = operStack.pop();res = numStack.cal(num1, num2, oper);numStack.push(res);}int finalRes = numStack.pop();System.out.println("表达式" +experssion+ "的结果是"+finalRes);}}
class ArrayStack {private int maxSize;private int[] stack;private int top = -1;//构造器public ArrayStack(int maxSize) {this.maxSize = maxSize;stack = new int[this.maxSize];}//栈满public boolean isFull() {return top == maxSize-1;}//栈空public boolean isEmpty() {return top == -1;}//入栈pushpublic void push(int value) {if(isFull()){System.out.println("栈满!");return;}top++;stack[top] = value;}//出栈pop,将栈顶的数据返回public int pop() {if(isEmpty()){throw new RuntimeException("栈空!"); //运行时异常,可捕获也可不捕获}int value = stack[top];top--;return value;}//遍历栈,遍历时,从栈顶开始显示数据public void showStack() {if(isEmpty()){System.out.println("栈为空,无法遍历!");}for (int i = top; i >= 0; i--) {System.out.printf("stack[%d] = %d\n",i,stack[i]);}}//增加一个方法,可以返回当前栈顶的值,但是不是真正的poppublic int peek(){return stack[top];}//返回运算符的优先级,优先级是程序员来确定,优先级使用数字表示//数字越大,优先级越高public int priority(int oper) { //在Java中int和char可混用if(oper == '*' || oper == '/') {return 1;}else if(oper == '+' || oper == '-'){return 0;}else{return -1;}}//判断是不是一个运算符public boolean isOper(char val){return val == '+' || val =='-' || val == '*' || val == '/';}//计算方法public int cal(int num1,int num2,int oper){int res = 0;switch (oper){case '+':res = num1 + num2;break;case '-':res = num2 - num1; //这里要注意顺序,2先进来,在前面,所以它是减数break;case '*':res = num2 * num1;break;case '/':res = num2 / num1;break;default:break;}return res;}
}

逆波兰计算器

我们完成一个逆波兰计算器,要求完成如下任务:

  1. 输入一个逆波兰表达式(后缀表达式),使用栈(Stack),计算其结果
  2. 支持小括号和多位数整数,因为这里我们主要讲的是数据结构,因此计算器进行简化,只支持对整数的计算。
  3. 思路分析:

后缀表达式计算机求值:

从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果

例如 (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:

  • 从左至右扫描,将3和4压入堆栈;
  • 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
  • 将5入栈;
  • 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
  • 将6入栈;
  • 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

代码实现:

package cn.ysk.example;import java.util.ArrayList;
import java.util.List;
import java.util.Stack;//逆波兰计算器
public class PolandBNotation {public static void main(String[] args) {//        String suffixExpression = "30 4 + 5 * 6 -"; ///(30+4)×5-6=>164String suffixExpression = "4 5 * 8 - 60 + 8 2 / +"; //4*5-8+60+8/2=76List<String> listString = getListString(suffixExpression);System.out.println(listString);int res = cal(listString);System.out.println("计算结果是:" + res);}//将一个逆波兰表达式,依次将数据和运算符放入到ArrayList中public static List<String> getListString(String suffixExpression) {String[] split = suffixExpression.split(" "); //将表达式分割List<String> list = new ArrayList<>();for (String s : split) {list.add(s);}return list;}//完成对逆波兰表达式的运算/*1)从左至右扫描,将3和4压入堆栈2)遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;3)将5入栈;4)接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;5)将6入栈;6)最后是-运算符,计算出35-6的值,即29,由此得出最终结果*/public static int cal(List<String> list) {Stack<String> stack = new Stack<>();for (String s : list) {if(s.matches("\\d+")) { /* \\d+ 匹配一个或多个数字,\\在正则中代表一个/ */stack.push(s);}else{/*遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈*/int num1 = Integer.parseInt(stack.pop()); //栈顶元素int num2 = Integer.parseInt(stack.pop()); //次顶元素int res = 0;if(s.equals("+")){res = num1 + num2;}else if(s.equals("-")){res = num2 - num1;}else if(s.equals("*")){res = num1 * num2;}else if(s.equals("/")){res = num2 / num1;}else{throw new RuntimeException("运算符有误!");}stack.push(String.valueOf(res)); //转换为字符串后入栈}}return Integer.parseInt(stack.pop()); //最后留在栈中的是运算结果}
}

中缀表达式转后缀表达式

大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式

具体步骤

  1. 初始化两个栈:运算符栈s1和储存中间结果的栈s2;

  2. 从左至右扫描中缀表达式;

  3. 遇到操作数时,将其压s2;

  4. 遇到运算符时,比较其与s1栈顶运算符的优先级:

    (1)如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;

    (2)否则,若优先级比栈顶运算符的高,也将运算符压入s1;

    (3)否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较

  5. 遇到括号时:

    (1) 如果是左括号“(”,则直接压入s1

    (2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃

  6. 重复步骤2至5,直到表达式的最右边

  7. 将s1中剩余的运算符依次弹出并压入s2

  8. 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式

举例说明

将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下:

扫描到的元素 s2(栈底->栈顶) s1 (栈底->栈顶) 说明
1 1 数字,直接入栈
+ 1 + s1为空,运算符直接入栈
( 1 + ( 左括号,直接入栈
( 1 + ( ( 同上
2 1 2 + ( ( 数字
+ 1 2 + ( ( + s1栈顶为左括号,运算符直接入栈
3 1 2 3 + ( ( + 数字
) 1 2 3 + + ( 右括号,弹出运算符直至遇到左括号
× 1 2 3 + + ( × s1栈顶为左括号,运算符直接入栈
4 1 2 3 + 4 + ( × 数字
) 1 2 3 + 4 × + 右括号,弹出运算符直至遇到左括号
- 1 2 3 + 4 × + - -与+优先级相同,因此弹出+,再压入-
5 1 2 3 + 4 × + 5 - 数字
到达最右端 1 2 3 + 4 × + 5 - s1中剩余的运算符

因此结果为 “1 2 3 + 4 × + 5 –”

代码实现(中缀转后缀与逆波兰综合版):

package cn.ysk.example;import java.util.ArrayList;
import java.util.List;
import java.util.Stack;public class ReversePolandNotaion {public static void main(String[] args) {//完成将一个中缀表达式转成后缀表达式的功能//说明// 1.1+((2+3)×4)-5=>转成123+4×+5–//2.因为直接对str进行操作,不方便,因此先将"1+((2+3)×4)-5"=》中缀的表达式对应的List,对list遍历轻松一点//即"1+((2+3)×4)-5"=>ArrayList[1,+,(,(,2,+,3,),*,4,),-,5]//3.将得到的中缀表达式对应的List=>后缀表达式对应的List//即ArrayList[1,+,(,(,2,+,3,),*,4,),-,5]=》ArrayList[1,2,3,+,4,*,+,5,–]String expression ="1+((2+3)*4)-5";//注意表达式List<String> infixExpressionList = toInfixExpressionList(expression);System.out.println("中缀表达式对应的list"+infixExpressionList);List<String> suffixExpressionList = parseSuffixExpressionList(infixExpressionList);System.out.println("后缀表达式对应的list:"+suffixExpressionList);int res = cal(suffixExpressionList);System.out.println("运算的结果是:" + res);}//方法:将得到的中缀表达式对应的List=>后缀表达式对应的Listpublic static List<String> parseSuffixExpressionList(List<String> ls) {//定义两个栈Stack<String> s1 = new Stack<>(); //符号栈//说明:因为s2这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出//因此比较麻烦,这里我们就不用Stack<String>直接使用List<String>s2List<String> s2 = new ArrayList<>(); //存贮中间结果的listfor (String item : ls) {if(item.matches("\\d+")){//是数字,就加入到s2s2.add(item);}else if("(".equals(item)){//如果是左括号“(”,则直接压入s1s1.push(item);}else if(")".equals(item)){//如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃while (!"(".equals(s1.peek())){ //.peek()查看栈顶元素s2.add(s1.pop());}s1.pop(); //将(弹出s1栈,消除小括号}else{//当s1栈顶运算符大于等于item的优先级,将s1栈顶的运算符弹出并加入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较while (s1.size()!=0 && Operaation.getValue(s1.peek()) >= Operaation.getValue(item)) {s2.add(s1.pop());}//还需要将item压入栈s1.push(item);}}//将s1中剩余的运算符依次弹出并加入s2while (s1.size() != 0){s2.add(s1.pop());}return s2;//注意因为是存放到List,因此按顺序输出就是对应的后缀表达式对应的List}//将中缀表达式转成对应的listpublic static List<String> toInfixExpressionList(String s) {List<String> list = new ArrayList<String>();String str; //字符串拼接int i = 0;char c;do{//如果是非数字,直接加入到listif((c=s.charAt(i)) < 48 || (c=s.charAt(i)) > 57){list.add(String.valueOf(c));i++;}else{str = "";while (i<s.length() && (c=s.charAt(i)) >= 48 && (c=s.charAt(i)) <= 57) {str+=c;//拼接i++;}list.add(str);}}while (i<s.length());return list;}//根据后缀表达式求值public static int cal(List<String> list) {Stack<String> stack = new Stack<>();for (String s : list) {if(s.matches("\\d+")) { /* \\d+ 匹配一个或多个数字,\\在正则中代表一个/ */stack.push(s);}else{/*遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈*/int num1 = Integer.parseInt(stack.pop()); //栈顶元素int num2 = Integer.parseInt(stack.pop()); //次顶元素int res = 0;if(s.equals("+")){res = num1 + num2;}else if(s.equals("-")){res = num2 - num1;}else if(s.equals("*")){res = num1 * num2;}else if(s.equals("/")){res = num2 / num1;}else{throw new RuntimeException("运算符有误!");}stack.push(String.valueOf(res)); //转换为字符串后入栈}}return Integer.parseInt(stack.pop()); //最后留在栈中的是运算结果}
}
class Operaation {private static int ADD = 1;private static int SUB = 1;private static int MUL = 1;private static int DIV = 1;//返回对应的优先级数字public static int getValue(String operation) {int res = 0;switch (operation){case "+":res = ADD;break;case "-":res = SUB;break;case "*":res = MUL;break;case "/":res = DIV;break;default:System.out.println("不存在该运算符!");break;}return res;}
}

第5章 递归

递归应用场景

看个实际应用场景,迷宫问题(回溯), 递归(Recursion)

递归的概念

简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。

递归调用机制

递归能解决什么样的问题

  1. 各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题
  2. 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
  3. 将用栈解决的问题–>第归代码比较简洁

递归需要遵守的重要规则

  1. 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
  2. 方法的局部变量是独立的,不会相互影响, 比如n变量。
  3. 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
  4. 递归必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError,死归了:)
  5. 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

递归-迷宫问题

背景介绍:

红色的方块不可到达,小球从位置(1,1)开始,走到(6,5)即为成功!

代码实现:

package cn.ysk.example;public class MiGong {public static void main(String[] args) {// 先创建一个二维数组,模拟迷宫// 地图int[][] map = new int[8][7];// 使用1 表示墙// 上下全部置为1for (int i = 0; i < 7; i++) {map[0][i] = 1;map[7][i] = 1;}// 左右全部置为1for (int i = 0; i < 8; i++) {map[i][0] = 1;map[6][0] = 1;}//设置挡板, 1 表示map[3][1] = 1;map[3][2] = 1;map[1][2] = 1;map[2][2] = 1;// 输出地图System.out.println("原地图是:");for (int i = 0; i < 8; i++) {for (int j = 0; j < 7; j++) {System.out.print(map[i][j] + " ");}System.out.println();}//使用递归回溯给小球找路setWay(map, 1, 1);//输出新的地图, 小球走过,并标识过的递归System.out.println("新地图是:");for (int i = 0; i < 8; i++) {for (int j = 0; j < 7; j++) {System.out.print(map[i][j] + " ");}System.out.println();}}//使用递归回溯来给小球找路//说明//1. map 表示地图//2. i,j 表示从地图的哪个位置开始出发 (1,1)//3. 如果小球能到 map[6][5] 位置,则说明通路找到.//4. 约定: 当map[i][j] 为 0 表示该点没有走过 当为 1 表示墙  ; 2 表示通路可以走 ; 3 表示该点已经走过,但是走不通//5. 在走迷宫时,需要确定一个策略(方法) 下->右->上->左 , 如果该点走不通,再回溯/**** @param map 表示地图* @param i 从哪个位置开始找* @param j* @return 如果找到通路,就返回true, 否则返回false*/public static boolean setWay(int[][] map,int i,int j){if(map[6][5] == 2) {return true;}else {if(map[i][j] == 0){ //表示未走过map[i][j] = 2; //假定该点可以走通if(setWay(map, i+1, j)) { //向下走return true;}else if(setWay(map, i, j+1)) { //向右走return true;}else if(setWay(map, i-1, j)) { //向上走return true;}else if(setWay(map, i, j-1)) { //向左走return true;}else {map[i][j] = 3;return false; //表示此路不通}}else {return false;}}}//修改找路的策略,改成 上->右->下->左
//  public static boolean setWay2(int[][] map, int i, int j) {//
//  }
}

对迷宫问题的讨论

  1. 小球得到的路径,和程序员设置的找路策略有关即:找路的上下左右的顺序相关
  2. 再得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
  3. 测试回溯现象

递归-八皇后问题(回溯算法)

回溯算法介绍

回溯法(英语:backtracking)是穷尽搜索算法(英语:Brute-force search)中的一种。

回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:

  • 找到一个可能存在的正确的答案
  • 在尝试了所有可能的分步方法后宣告该问题没有答案

在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。

通俗的来讲:解决问题时,每进行一步,都是抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目标,立刻做回退操作重新选择。这种走不通就回退再走的方法就是回溯法。

回溯VS递归

很多人认为回溯和递归是一样的,其实不然。在回溯法中可以看到有递归的身影,但是两者是有区别的。

回溯法从问题本身出发,寻找可能实现的所有情况。和穷举法的思想相近,不同在于穷举法是将所有的情况都列举出来以后再一一筛选,而回溯法在列举过程如果发现当前情况根本不可能存在,就停止后续的所有工作,返回上一步进行新的尝试。

递归是从问题的结果出发,例如求 n!,要想知道 n!的结果,就需要知道 n*(n-1)!的结果而要想知道 (n-1)! 结果,就需要提前知道 (n-1) * (n-2)!。这样不断地向自己提问,不断地调用自己的思想就是递归。

回溯和递归唯一的联系就是,回溯法可以用递归思想实现。

八皇后问题介绍

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

八皇后问题思路分析

用回溯的思想解决:

假设某一行为当前状态,不断检查该行所有的位置是否能放一个皇后,检索的状态有两种:

  • 先从首位开始检查,如果不能放置,接着检查该行第二个位置,依次检查下去,直到在该行找到一个可以放置一个皇后的地方,然后保存当前状态,转到下一行重复上述方法的检索。
  • 如果检查了该行所有的位置均不能放置一个皇后,说明上一行皇后放置的位置无法让所有的皇后找到自己合适的位置,因此就要回溯到上一行,重新检查该皇后位置后面的位置。

详细思路:

  1. 第一个皇后先放第一行第一列

  2. 第二个皇后放在第二行第一列、然后判断是否OK, 如果不OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适

  3. 继续第三个皇后,还是第一列、第二列……直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解

  4. 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到.

    (tip:,以4皇后为例,若此时已经得到一个正确结果1302,则会回溯,变为1303判断是否可行······)

  5. 然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4的步骤

  6. 实例:

八皇后问题代码实现

说明:

理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题.arr[8]={0,4,7,5,2,6,1,3}//对应arr下标表示第几行,即第几个皇后,arr[i]=val,val表示第i+1个皇后,放在第i+1行的第val+1列 arr[1] = 4,表示在第2行的第5列放置。

代码:

package cn.ysk.example;public class Queue8 {int max = 8;int[] arr = new int[max];  //定义数组array, 保存皇后放置位置的结果,比如 arr = {0 , 4, 7, 5, 2, 6, 1, 3}static int count = 0; //统计有多少组解static int calCount = 0; //统计判断冲突的次数public static void main(String[] args) {Queue8 queue8 = new Queue8();queue8.check(0);System.out.println("解的个数:"+count);System.out.println("判断冲突次数:"+ calCount);}//编写一个方法,放置第n个皇后//特别注意: check 是 每一次递归时,进入到check中都有  for(int i = 0; i < max; i++),因此会有回溯public void check(int n) {if(n == 8){ //结束条件print();return;}//依次放入皇后,并判断是否冲突for (int i = 0; i < max; i++) {//先把当前这个皇后 n , 放到该行的第1列arr[n] = i;if(judge(n)) { //判断是否冲突,不冲突就向下进行//接着放n+1个皇后,即开始递归check(n+1);}//如果冲突,就继续执行 array[n] = i; 即将第n个皇后,放置在本行得 后移的一个位置}}public boolean judge(int n){ //判断此点与之前的每个点是否冲突calCount++;for (int i = 0; i < n; i++) {//arr[i] == arr[n]是判断是否在一条直线上,//Math.abs(n-i) == Math.abs(arr[n] - arr[i])是判断是否在一条斜线上(根据斜率来看,可理解为两条边相等// 则是等腰直角三角形,斜率为1,另外相对位置变化多样,要加上绝对值)//是否在同一行, 没有必要判断,n 每次都在递增if(arr[i] == arr[n] || Math.abs(n-i) == Math.abs(arr[n] - arr[i])) {return false;}}return true;}//    将皇后摆放的位置输出public void print() {count++;for (int i = 0; i < arr.length; i++) {System.out.print(arr[i] + " ");}System.out.println();}
}

第6章 排序算法

排序算法的介绍

排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。

排序算法的分类

  1. 内部排序:

    指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。

  2. 外部排序法:

    数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。

  3. 常见的排序算法分类:

算法的时间复杂度

度量一个程序(算法)执行时间的两种方法

  • 事后统计的方法

    这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。

  • 事前估算的方法

    通过分析某个算法的时间复杂度来判断哪个算法更优.

时间频度

基本介绍:

时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。[举例说明]

  1. 举例说明-基本案例

    比如计算1-100所有数字之和, 我们设计两种算法:

    T(n)=n+1;

    T(n)=1

  2. 举例说明-忽略常数项

    T(n)=2n+20 T(n)=2*n T(3n+10) T(3n)
    1 22 2 13 3
    2 24 4 16 6
    5 30 10 25 15
    8 36 16 34 24
    15 50 30 55 45
    30 80 60 100 90
    100 220 200 310 300
    300 620 600 910 900

结论:

  1. 2n+20 和 2n 随着n 变大,执行曲线无限接近, 20可以忽略

  2. 3n+10 和 3n 随着n 变大,执行曲线无限接近, 10可以忽略

  3. 举例说明-忽略低次项

    T(n)=2n^2+3n+10 T(2n^2) T(n^2+5n+20) T(n^2)
    1 15 2 26 1
    2 24 8 34 4
    5 75 50 70 25
    8 162 128 124 64
    15 505 450 320 225
    30 1900 1800 1070 900
    100 20310 20000 10520 10000

结论:

  1. 2n^2+3n+10 和 2n^2 随着n 变大, 执行曲线无限接近, 可以忽略 3n+10

  2. n^2+5n+20 和 n^2 随着n 变大,执行曲线无限接近, 可以忽略 5n+20

  3. 举例说明-忽略系数

T(3n^2+2n) T(5n^2+7n) T(n^3+5n) T(6n^3+4n)
1 5 12 6 10
2 16 34 18 56
5 85 160 150 770
8 208 376 552 3104
15 705 1230 3450 20310
30 2760 4710 27150 162120
100 30200 50700 1000500 6000400

结论:

  1. 随着n值变大,5n^2+7n 和 3n^2 + 2n ,执行曲线重合, 说明 这种情况下, 5和3可以忽略。
  2. 而n^3+5n 和 6n^3+4n ,执行曲线分离,说明多少次方式关键

时间复杂度

  1. 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度
  2. T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。
  3. 计算时间复杂度的方法:
    • 用常数1代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1
    • 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = n²
    • 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)

常见的时间复杂度

  • 常数阶O(1)
  • 对数阶O(log2n)
  • 线性阶O(n)
  • 线性对数阶O(nlog2n)
  • 平方阶O(n^2)
  • 立方阶O(n^3)
  • k次方阶O(n^k)
  • 指数阶O(2^n)

说明:

  • 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <Ο(2n) ,随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低
  • 从图中可见,我们应该尽可能避免使用指数阶的算法
  1. 常数阶O(1)

    无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)

上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。

  1. 对数阶O(log2n)

说明:在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2n也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(log2n) 。 O(log2n) 的这个2 时间上是根据代码变化的,i = i * 3 ,则是 O(log3n) .

  1. 线性阶O(n)

说明:这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度

  1. 线性对数阶O(nlogN)

    说明:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)

  2. 平方阶O(n²)

说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n * n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n)

  1. 立方阶O(n³)、K次方阶O(n^k)

    说明:参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似

平均时间复杂度和最坏时间复杂度

  1. 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
  2. 最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
  3. 平均时间复杂度和最坏时间复杂度是否一致,和算法有关。

算法的空间复杂度简介

  1. 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
  2. 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
  3. 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间.

冒泡排序

基本思想

冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较
相邻元素的值,若发现逆序则交换
,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。

优化:因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,再进行)

过程图解

操作数的过程称为冒泡,一轮次的冒泡操作,能使数列最后的数成为最大值。

若有n个数,则需要进行n-1轮次的比较,在第1轮次中要进行n-1次两两比较,在第i轮次的比较中要进行n-i次两两比较。

稳定性:冒泡排序稳定!

应用实例

冒泡排序的代码实现:

 public void bubbleSort(int arr[]) {int temp = 0;//n个关键字,最多需要n-1次冒泡处理for (int i = 0; i < arr.length-1; i++) {//对于n个关键字,在第i趟中,进行n-i次比较for (int j = 0; j < arr.length-i-1; j++) {if(arr[j] > arr[j+1]) {temp = arr[j+1];arr[j+1] = arr[j];arr[j] = temp;}}System.out.println("第"+ (i+1) +"趟排序后的数组:");System.out.println(Arrays.toString(arr));}}

完整代码:

package cn.ysk.sort;import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;public class BubbleSort {public static void main(String[] args) {int[] arr = {3, 9, -1, 20, 10};
//        int[] arr = new int[80000];
//        for (int i = 0; i < 80000; i++) {//            arr[i] = (int) (Math.random()*80000);
//        }
//        Date date = new Date();
//        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        String pTime = simpleDateFormat.format(date);
//        System.out.println("之间的时间:"+ pTime);BubbleSort bubbleSort = new BubbleSort();bubbleSort.bubbleSort(arr);
//        Date date1 = new Date();
//        String lTime = simpleDateFormat.format(date1);
//        System.out.println("后来的时间:"+lTime);  //大概10s}//冒泡排序public void bubbleSort(int arr[]) {int temp = 0;for (int i = 0; i < arr.length-1; i++) {for (int j = 0; j < arr.length-i-1; j++) {if(arr[j] > arr[j+1]) {temp = arr[j+1];arr[j+1] = arr[j];arr[j] = temp;}}System.out.println("第"+ (i+1) +"趟排序后的数组:");System.out.println(Arrays.toString(arr));}}//优化后的冒泡排序public void bubbleSort2(int arr[]){int temp = 0;boolean flag = false;for (int i = 0; i < arr.length; i++) {for (int j = 0; j < arr.length-i-1; j++) {if(arr[j] > arr[j+1]) {flag = true;temp = arr[j+1];arr[j+1] = arr[i];arr[i] = temp;}System.out.println("第"+ i+1 +"趟排序后的数组:");System.out.println(Arrays.toString(arr));}if(!flag) { //若在某一趟中没有进行任何变换则证明已排好序,直接退出break;}else{flag = false;  //将flag重新赋为false,不然一直为true,在后面发生上述特殊情况时,无法进到if退出}}}
}

选择排序

基本思想

对于一个待排序的数列,首先从n个数据中选择一个最小的数据并将它交换到第一个位置;然后再从剩下的n-1个数据中选择一个最小的数据,并将它交换到第二个位置;依次类推,直至最后从两个数据中选择一个最小的数据,并将它交换到第n-1个位置为止。若有n个数,则需要进行n-1次选择操作。(将最小的数据放到最前方)。

过程图解

稳定性:选择排序不稳定:举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

应用实例

代码实现:

 public static void selectSort(int arr[]) {int min = 0;int temp = 0;for (int i = 0; i < arr.length-1; i++) {min = i;for (int j = i+1; j < arr.length; j++) {if(arr[j] < arr[i]) {min = j;}}temp = arr[i];arr[i] = arr[min];arr[min] = temp;System.out.println("第"+(i+1)+"趟的数组是:");System.out.println(Arrays.toString(arr));}}

完整代码:

package cn.ysk.sort;import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;public class SelectSort {public static void main(String[] args) {int[] arr = {3, 9, -1, 20, 10};
//        int[] arr = new int[80000];
//        for (int i = 0; i < 80000; i++) {//            arr[i] = (int) (Math.random()*80000);
//        }
//        Date date = new Date();
//        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        String pTime = simpleDateFormat.format(date);
//        System.out.println("之间的时间:"+ pTime);
//selectSort(arr);
//        Date date1 = new Date();
//        String lTime = simpleDateFormat.format(date1);
//        System.out.println("后来的时间:"+lTime); //大概3s}public static void selectSort(int arr[]) {int min = 0;int temp = 0;for (int i = 0; i < arr.length-1; i++) {min = i;for (int j = i+1; j < arr.length; j++) {if(arr[j] < arr[i]) {min = j;}}temp = arr[i];arr[i] = arr[min];arr[min] = temp;System.out.println("第"+(i+1)+"趟的数组是:");System.out.println(Arrays.toString(arr));}}
}

插入排序

基本思想

把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

过程图解

稳定性:插入排序稳定!

应用实例

代码实现:

 public static void insertSort(int[] arr) {// 给insertVal 找到插入的位置// 说明// 1. insertIndex >= 0 保证在给insertVal 找插入位置,不越界// 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置// 3. 就需要将 arr[insertIndex] 后移int insertValue = 0;int insertIndex = 0;for (int i = 1; i < arr.length; i++) {insertValue = arr[i];insertIndex = i - 1;while (insertIndex >= 0 && insertValue < arr[insertIndex]) {arr[insertIndex + 1] = arr[insertIndex];insertIndex--;}//上面也可以用for循环来写
//            for ( insertIndex = i-1; insertIndex >= 0 && insertValue < arr[insertIndex]; insertIndex--) {//                arr[insertIndex + 1] = arr[insertIndex];
//            }//insertIndex+1 才是插入的位置,用极端值理解,若位置没有变化(i=1),insertIndex=0,插入的位置还是1,所以要加1if(insertIndex + 1 != i) { //若等于i,证明位置没有变动,不需要执行下面的语句arr[insertIndex + 1] = insertValue;}System.out.println("第"+ i +"趟的数组是:");System.out.println(Arrays.toString(arr));}}

完整代码:

package cn.ysk.sort;import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;public class InsertSort {public static void main(String[] args) {int[] arr = {3, 9, -1, 20, 10,16,8}; //80000个数据一秒不到insertSort(arr);
//        getTime();}public static void insertSort(int[] arr) {// 给insertVal 找到插入的位置// 说明// 1. insertIndex >= 0 保证在给insertVal 找插入位置,不越界// 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置// 3. 就需要将 arr[insertIndex] 后移int insertValue = 0;int insertIndex = 0;for (int i = 1; i < arr.length; i++) {insertValue = arr[i];insertIndex = i - 1;while (insertIndex >= 0 && insertValue < arr[insertIndex]) {arr[insertIndex + 1] = arr[insertIndex];insertIndex--;}//上面也可以用for循环来写
//            for ( insertIndex = i-1; insertIndex >= 0 && insertValue < arr[insertIndex]; insertIndex--) {//                arr[insertIndex + 1] = arr[insertIndex];
//            }//insertIndex+1 才是插入的位置,用极端值理解,若位置没有变化(i=1),insertIndex=0,插入的位置还是1,所以要加1if(insertIndex + 1 != i) { //若等于i,证明位置没有变动,不需要执行下面的语句arr[insertIndex + 1] = insertValue;}System.out.println("第"+ i +"趟的数组是:");System.out.println(Arrays.toString(arr));}}public static void getTime(){int[] arr = new int[80000];for (int i = 0; i < 80000; i++) {arr[i] = (int) (Math.random()*80000);}Date date = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String pTime = simpleDateFormat.format(date);System.out.println("之间的时间:"+ pTime);insertSort(arr);Date date1 = new Date();String lTime = simpleDateFormat.format(date1);System.out.println("后来的时间:"+lTime); //大概3s}
}

希尔排序

简单插入排序存在的问题

数组arr={2,3,4,5,6,1}这时需要插入的数1(最小),这样的过程是:

​ {2,3,4,5,6,6}
​ {2,3,4,5,5,6}
​ {2,3,4,4,5,6}
​ {2,3,3,4,5,6}
​ {2,2,3,4,5,6}
​ {1,2,3,4,5,6}

结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.

希尔排序法介绍

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本(升级版的插入排序),也称为缩小增量排序

希尔排序的基本思想

希尔排序是定义一个间隔序列来表示排序过程中进行比较的元素之间有多远的间隔,每次将具有相同间隔的数分为一组,进行插入排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

希尔排序的实质就是分组的插入排序

过程图解

注意:

对各个组进行插入的时候并不是先对一个组进行排序完再对另一个组进行排序,而是轮流对每个组进行插入排序。(gap每次+1体现了这一点),图示说明:

稳定性:希尔排序不稳定:一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

推荐的博文:

应用实例

代码实现:

 //对交换式的希尔排序进行优化->移位法public static void shellSort2(int[] arr) { //80000个数据16毫秒!int count = 0;for (int gap = arr.length /2; gap > 0 ; gap/=2) {for (int i = gap; i < arr.length; i++) {int inserVal = arr[i];int j;for ( j = i-gap; j >= 0 && inserVal < arr[j]; j-=gap) { //此处联系插入排序进行思考arr[j+gap] = arr[j];}arr[j+gap] = inserVal;}System.out.println("第"+(++count)+"轮次的数组是:");System.out.println(Arrays.toString(arr));}

完整代码:

package cn.ysk.sort;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;public class ShellSort {public static void main(String[] args) {int[] arr = new int[8];for (int i = 0; i < 8; i++) {arr[i] = (int) (Math.random()*80000);}
//        Date date = new Date();
//        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        String pTime = simpleDateFormat.format(date);long pTime =  System.currentTimeMillis();System.out.println("之前的时间:"+ pTime);shellSort(arr);
//        Date date1 = new Date();
//        String lTime = simpleDateFormat.format(date1);long lTime =  System.currentTimeMillis();System.out.println("后来的时间:"+lTime);System.out.println("最终时间:" + (lTime-pTime)+"ms");//        shellSort(arr); //交换式System.out.println(Arrays.toString(arr));
//        shellSort2(arr);//移位方式
//        getTime();}//两种方法// 使用逐步推导的方式来编写希尔排序// 希尔排序时, 对有序序列在插入时采用交换法,// 思路(算法) ===> 代码public static void shellSort(int[] arr) {  //80000个数据5秒int temp = 0;int count = 0;for (int gap = arr.length/2; gap > 0 ; gap/=2) {for (int i = gap; i < arr.length; i++) {for (int j = i-gap; j >= 0 ; j-=gap) {if(arr[j] > arr[j+gap]) {temp = arr[j];arr[j] = arr[j+gap];arr[j+gap] = temp;}}}System.out.println("在"+(++count)+"轮次中的数组是:");System.out.println(Arrays.toString(arr));}}//对交换式的希尔排序进行优化->移位法public static void shellSort2(int[] arr) { //80000个数据16毫秒!int count = 0;for (int gap = arr.length /2; gap > 0 ; gap/=2) {for (int i = gap; i < arr.length; i++) {int inserVal = arr[i];int j;for ( j = i-gap; j >= 0 && inserVal < arr[j]; j-=gap) { //此处联系插入排序进行思考arr[j+gap] = arr[j];}arr[j+gap] = inserVal;}System.out.println("第"+(++count)+"轮次的数组是:");System.out.println(Arrays.toString(arr));}}public static void getTime(){int[] arr = new int[8];for (int i = 0; i < 8; i++) {arr[i] = (int) (Math.random()*8);}
//        Date date = new Date();
//        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        String pTime = simpleDateFormat.format(date);long pTime =  System.currentTimeMillis();System.out.println("之间的时间:"+ pTime);shellSort(arr);
//        Date date1 = new Date();
//        String lTime = simpleDateFormat.format(date1);long lTime =  System.currentTimeMillis();System.out.println("后来的时间:"+lTime);System.out.println("最终时间:" + (lTime-pTime)+"ms");System.out.println(Arrays.toString(arr));}}

快速排序

基本思想

快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一轮的排序将序列分割成独立的两部分,其中一部分序列的关键字(这里主要用值来表示)均比另一部分关键字小。继续对长度较短的序列进行同样的分割,最后到达整体有序。在排序过程中,由于已经分开的两部分的元素不需要进行比较,故减少了比较次数,降低了排序时间。

快速排序通过对序列不断划分,把原始序列以划分元素为界形成两个子序列,再对子序列重复划分过程。这显然是一个递归的过程,递归的终止条件是子序列中只含有一个元素。在每次划分的过程中,需要设置前后两个指针,这两个指针依次往序列中间位置移
动,当指针重合时,结束本次划分。(使用了分治策略!)

过程图解

一般是以第一个数为基准数

上面是快速排序的基本过程,接下来是每次划分的具体的操作:

这里有两种思想可供理解:

先从后往前找,再从前往后找!

  • 左右指针法

序列: 3 1 2 5 4 6 9 7 10 8

  1. 分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即j=10),指向数字8。

  2. 首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。

  3. 现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下: 6 1 2 5 9 3 4 7 10 8 。 到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。

  1. 此时再次进行交换,交换之后的序列如下:6 1 2 5 4 3 9 7 10 8。 第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下: 3 1 2 5 4 6 9 7 10 8

  1. 到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。

  2. 此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来按照上述方法分别处理这两个序列即可。

上述过程来源于

  • 挖坑填数法

    不再赘述,推荐用一个好的博文去理解

稳定性:在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。

关于快速排序的几个问题

此段来源于知乎:[排序–快速排序]: https://zhuanlan.zhihu.com/p/93129029

  1. 快排为啥叫快排,快排是所有排序里面性能最好的吗?

  2. 快排适合什么情况呢,还是无论什么情况下快排总是最好的(显然× ?

    上面两个问题的答案:

    快排的性能在所有排序算法里面是最好的,数据规模越大快速排序的性能越优。快排在极端情况下会退化成 的算法,因此假如在提前得知处理数据可能会出现极端情况的前提下,可以选择使用较为稳定的归并排序。

  3. 快排算法性能优良的原因是依赖于算法中的哪个部分?

    • 首先,如果我们已经知道 a<b ,那么之后 a,b 就无需再比较了,假如再进行比较就是重复在快排中其中有一个一定是基准,因此假如进行一次比较之后就不会再次进行比较了。
    • 第二,假如我们已知 a<b<c ,那么 a,c 就不需要进行比较了,再进行比较就算是冗余,而在快排中这种情况是不会发生的,因为 b就是递归排序的基准,因此 a,c 就只会在自己的区间进行排序,不会出现冗余排序了。

    因此,我们可以了解到,快速排序的优越性体现在他没有多余的比较上,对于初学者,我们可以较为简单的认为,快排所需要的指令数会比较少。

应用实例

代码实现:

 public static void quickSort(int arr[],int left,int right) {if(left<right){int base = arr[left];int i = left;int j = right;int temp;while (i<j) {while (arr[j] >= base && i<j) {j--;}while (arr[i] <= base && i<j) {i++;}//找到后,就交换if(i<j) {temp = arr[j];arr[j] = arr[i];arr[i] = temp;}}arr[left] = arr[i];arr[i] = base;quickSort(arr, left, i-1);quickSort(arr, i+1, right);}}

完整代码:

package cn.ysk.sort;import java.util.Arrays;public class QuickSort {public static void main(String[] args) {int[] arr = {6,-1,2,7,12,3,4,5,10,8};int l = 0;int r = arr.length - 1;quickSort(arr, l, r);System.out.println(Arrays.toString(arr));
//        getTime();}public static void quickSort(int arr[],int left,int right) {if(left<right){int base = arr[left];int i = left;int j = right;int temp;while (i<j) {while (arr[j] >= base && i<j) {j--;}while (arr[i] <= base && i<j) {i++;}//找到后,就交换if(i<j) {temp = arr[j];arr[j] = arr[i];arr[i] = temp;}}arr[left] = arr[i];arr[i] = base;System.out.println("left:"+left+",i:"+i);System.out.println(Arrays.toString(arr));System.out.println();quickSort(arr, left, i-1);quickSort(arr, i+1, right);}}public static void getTime(){ //运行80000个数据耗时:25ms,运行800000个数据耗时:97msint[] arr = new int[800000];for (int i = 0; i < 800000; i++) {arr[i] = (int) (Math.random()*80000);}
//        Date date = new Date();
//        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        String pTime = simpleDateFormat.format(date);
//        System.out.println("之间的时间:"+ pTime);long startTime = System.currentTimeMillis();int l = 0;int r = arr.length - 1;quickSort(arr, l, r);
//        Date date1 = new Date();
//        String lTime = simpleDateFormat.format(date1);
//        System.out.println("后来的时间:"+lTime); //long endTime = System.currentTimeMillis();System.out.println("运行"+arr.length+"个数据耗时:"+(endTime-startTime)+"ms");
//        System.out.println(Arrays.toString(arr));}
}

归并排序

基本思想

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

过程图解

归并排序思想示意图1-基本思想:

归并排序思想示意图2-合并相邻有序子序列:

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤


动态展示:

来源于菜鸟教程

归并排序原来就是将一堆数字分开,再合成有序的数列。这就是分治的思想,将大问题化小问题,将每个最小的问题处理好,合并起来大问题也就处理好了。

乍一看,归并排序是一种“费力不讨好”的排序方法,因为最后一趟始终要对整个序列进行排序(这种情况不是第一次遇到,回想希尔排序的特点),这会使得前几趟的排序似乎是在做无用功,其实不然。对初始关键字两两分组并进行组内排序后,在下一次处理中,并不是简单地在组容量扩大一倍的基础上重新排序,而是把上一趟已经排好序的再组数组重新合并成一个新的有序组。这个把两个有序组合并成一个新的有序组的过程要比单独排序快得多。归并排序的关键是合并有序组、对于最开始的两两分组,也可以看成是对两个只含有1个关键字的组进行合并。

除了关键的合并操作外,需要先把序列进行分组,每次组容量减半,直到组内只有一个关键字为止,再对组进行西两合并,直到所有关键字都属于一组为止。

稳定性:归并排序稳定

应用实例

代码实现:

package cn.ysk.sort;import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;public class MergeSort {public static void main(String[] args) {//        int arr[] = { 8, 4, 5, 7, 1, 3 ,6,2};
//        int[] temp = new int[arr.length];getTime();
//        System.out.println("归并排序后=" + Arrays.toString(arr));}//分+合方法public static void mergeSort(int[] arr, int left, int right, int[] temp) {if(left<right) { //组内的数据大于1时需要排序int mid = (left+right)/2;mergeSort(arr, left, mid, temp);mergeSort(arr, mid+1, right, temp);merge(arr, left, mid, right, temp);}}//合并的方法/**** @param arr 排序的原始数组* @param left 左边有序序列的初始索引* @param mid 中间索引* @param right 右边索引* @param temp 做中转的数组*/public static void merge(int[] arr, int left, int mid, int right, int[] temp) {int i = left;int j = mid+1;int t = 0;while (i <= mid && j <= right) {if(arr[i] <= arr[j]) {temp[t] = arr[i];  //可简写arr[t++] = arr[i++]t++;i++;}else {temp[t] = arr[j];t++;j++;}}while (i<=mid) {temp[t] = arr[i];t++;i++;}while (j<=right) {temp[t] = arr[j];t++;j++;}t = 0;int tempLeft = left;
//        System.out.println("tempLeft:"+tempLeft+" right:"+ right);while (tempLeft<=right) {arr[tempLeft] = temp[t];t++;tempLeft++;}
//        System.out.println(Arrays.toString(temp));}public static void getTime(){ //运行80000个数据耗时:13ms,运行800000个数据耗时:107msint[] arr = new int[800000];for (int i = 0; i < 800000; i++) {arr[i] = (int) (Math.random() * 80000); // 生成一个[0, 8000000) 数}long startTime = System.currentTimeMillis();
//        Date data1 = new Date();
//        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        String date1Str = simpleDateFormat.format(data1);
//        System.out.println("排序前的时间是=" + date1Str);int temp[] = new int[arr.length]; //归并排序需要一个额外空间mergeSort(arr, 0, arr.length - 1, temp);long endTime = System.currentTimeMillis();
//        Date data2 = new Date();
//        String date2Str = simpleDateFormat.format(data2);
//        System.out.println("排序前的时间是=" + date2Str);System.out.println("运行"+arr.length+"个数据耗时:"+(endTime-startTime)+"ms");
//        System.out.println("归并排序后=" + Arrays.toString(arr));}
}

基数排序

基数排序介绍

  • 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
  • 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
  • 基数排序(Radix Sort)是桶排序的扩展
  • 基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基数排序的基本思想

基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

过程图解



稳定性:基数排序稳定

基数排序的说明

  • 基数排序是对传统桶排序的扩展,速度很快.
  • 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
  • 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
  • 有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考: https://code.i-harness.com/zh-CN/q/e98fa9

应用实例

代码实现:

package cn.ysk.sort;import java.util.Arrays;public class RadixSort {public static void main(String[] args) {//        int arr[] = { 53, 3, 542, 748, 14, 214};
//        radixSort(arr);
//        System.out.println("基数排序后 " + Arrays.toString(arr));getTime();}public static void radixSort(int arr[]) {//得到数组中最大的数,确定进行个位,十位,百位……int max  = arr[0];for (int i = 1; i < arr.length; i++) {if(arr[i] > max) {max = arr[i];}}int maxLength = (""+max).length();//int maxLength = String.valueOf(max).length();//定义一个二维数组,表示10个桶, 每个桶就是一个一维数组//说明//1. 二维数组包含10个一维数组//2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length//3. 基数排序是使用空间换时间的经典算法int[][] bucket = new int[10][arr.length];//为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数//可以这里理解(为后面的取出做准备)//比如:bucketElementCounts[0] , 记录的就是第一个 桶的放入数据个数int[] bucketElementCounts = new int[10];for (int i = 0,n = 1; i < maxLength; i++,n*=10) {//(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..for (int j = 0; j < arr.length; j++) {int digitOfElement = arr[j] /n %10; //得到相应位数的值,748 / 10 => 74 % 10 => 4//bucket[digitOfElement]:放的是第n个桶//[bucketElementCounts[digitOfElement]]:放的是第n个桶中的哪个位置,初始值是0bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];bucketElementCounts[digitOfElement]++;}int index = 0;//遍历这10个桶,并将桶中的数据,放入到原数组for (int j = 0; j < bucketElementCounts.length; j++) {if(bucketElementCounts[j] != 0) {  //桶中有数据就取出来for (int k = 0; k < bucketElementCounts[j]; k++) {//遍历这个桶,将桶中的第k个数据取出arr[index] = bucket[j][k];index++;}}//第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!bucketElementCounts[j] = 0;}
//            System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));}}public static void getTime() {  //运行80000个数据耗时:22msint[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random()*80000);}long startTime = System.currentTimeMillis();radixSort(arr);long endTime = System.currentTimeMillis();System.out.println("运行"+arr.length+"个数据耗时:"+(endTime-startTime)+"ms");
//        System.out.println(Arrays.toString(arr));}
}

常用排序算法总结和对比

通过上图可以看出,**没有哪种算法“绝对的优秀”.**在解决实际问题时,要根据不同的需求和数据特点选择合适的排序算法,在选择排序算法时一般遵循以下原则:

  • 排序规模不大,用直接插入排序、简单选择排序、冒泡排序均可,虽然其时间复杂度逊于快速排序等算法,但其实现简单,性能的差距在数据量较小时体现不明显。
  • 综合表现,快速排序最佳,这也符合“快速”的称号,虽然其在空间复杂度方面逊于堆排序等算法,且在序列有序的情况下,快速排序也逊于堆排序,但其在编程的复杂性上比堆排序简单。
  • 当排序规模很大,而且对稳定性有要求时,可以采用归并排序,前提是有足够的辅助空间。
  • 当排序规模很大而关键字位数较小时,可以采用基数排序,速度有保证且稳定。

不稳定的排序算法有:快、希、选、堆。(记忆:有钱了就可以“快些选一堆”……

数据结构和算法(Java),上相关推荐

  1. 数据结构与算法-java笔记一 更新中

    数据结构与算法-java笔记一 更新中 数据结构与算法 什么是数据结构.算法 数据结构学了有什么用: 线性结构 数组 特点 应用 链表 存储结构 链表类型 单链表 双向链表 双向循环链表 链表与数组的 ...

  2. 视频教程-内功修炼之数据结构与算法-Java

    内功修炼之数据结构与算法 2018年以超过十倍的年业绩增长速度,从中高端IT技术在线教育行业中脱颖而出,成为在线教育领域一匹令人瞩目的黑马.咕泡学院以教学培养.职业规划为核心,旨在帮助学员提升技术技能 ...

  3. 二叉查找树(1)-二叉树-数据结构和算法(Java)

    文章目录 1 前言 1.1 二叉查找树定义 1.2 二叉查找树的性质 2 基本实现 2.1 API 2.2 实现代码 2.2.1 数据表示 2.2.2 查找 2.2.3 插入 3 分析 4 有序性相关 ...

  4. 0302Prim算法-最小生成树-图-数据结构和算法(Java)

    文章目录 1 Prim算法 1.1 概述 1.1.1 算法描述 1.1.2 数据结构 1.1.3 横切边集合维护 1.2 延时实现 1.2.1 实现代码 1.2.2 性能分析 1.3 即时实现 1.3 ...

  5. 数据结构与算法Java(二)——字符串、矩阵压缩、递归、动态规划

    不定期补充.修正.更新:欢迎大家讨论和指正 本文以数据结构(C语言版)第三版 李云清 杨庆红编著为主要参考资料,用Java来实现 数据结构与算法Java(一)--线性表 数据结构与算法Java(二)- ...

  6. 数据结构和算法(Java)-张晨光-专题视频课程

    数据结构和算法(Java)-579人已学习 课程介绍         如果说各种编程语言是程序员的招式,那么数据结构和算法就相当于程序员的内功. 想写出精炼.优秀的代码,不通过不断的锤炼,是很难做到的 ...

  7. 02优先队列和索引优先队列-优先队列-数据结构和算法(Java)

    文章目录 1 概述 1.1 需求 1.2 优先队列特点 1.3 优先队列分类 1.4 应用场景 1.5 相关延伸 2 说明 3 索引优先队列 3.1 实现思路 3.2 API设计 3.2 代码实现及简 ...

  8. 数组【数据结构与算法Java】

    数组[数据结构与算法Java] 数组 数组 略

  9. 【数据结构与算法-java实现】一 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?

    今天开始学习程序的灵魂:数据结构与算法. 本文是自己学习极客时间专栏-数据结构与算法之美后的笔记总结.如有侵权请联系我删除文章. 我们都知道,数据结构和算法本身解决的是"快"和&q ...

  10. 【转】数据结构与算法(上)

    数据结构是以某种形式将数据组织在一起的集合,它不仅存储数据,还支持访问和处理数据的操作.算法是为求解一个问题需要遵循的.被清楚指定的简单指令的集合.下面是自己整理的常用数据结构与算法相关内容,如有错误 ...

最新文章

  1. 六大“未来式”存储器,谁将脱颖而出?
  2. 大数据可以帮助企业获得资金吗?
  3. vue.js----之router详解(三)
  4. logging模块的使用
  5. 【设计模式】前端必懂EventEmitter
  6. Firebug的安装方法
  7. wav音量和分贝转换关系_电吉他音箱瓦数与音量大小之间的关系
  8. case函数,replace函数
  9. 断开式 DataGridView控件 winform
  10. @EnableWebMvc启动springmvc特性
  11. gpio 树莓派3a+_树莓派4正式发布:35美元起售!真香
  12. lock.ReadWriteLock使用方法
  13. windows 下安装linux子系统及其可视化【Linux】
  14. Nero Burning Rom v7.2.3.2b 简体中文版
  15. Swarm(bzz)主网于6月21日正式启动 ,BZZ币合约已部署?红利提前来了吗?
  16. iPhone6 6p 7 7p屏幕适配,切图准则
  17. 如何定义和使用一个 Lambda 表达式
  18. 论文Express | 谷歌DeepMind最新动作:使用强化对抗学习,理解绘画笔触
  19. PMP-商业论证中的财务测量指标-动态投资回收期、净现值、内部收益率、效益成本率计算
  20. 自动化学报Ctex+texstudio配置方法

热门文章

  1. 记忆里:小时候的农村青山绿水,鸟语花香,彩蝶飞飞
  2. 蚂蚁金服缘何自研Service Mesh?
  3. 国产FPGA高云GW1NSR-4C,集成ARM Cortex-M3硬核
  4. Neo4j 启动报错 Server shutdown initiated by request
  5. PHP上传Excel文件
  6. 【大数据 BI】传统BI流程
  7. workbench设置单元坐标系_浅谈Ansys中的几种坐标系
  8. vdagent与vdserver
  9. IPCam的启动过程和工作流程
  10. 淘宝技术发展(Oracle/支付宝/旺旺)