数据结构和算法(Java),上

文章目录

  • 第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章 稀疏数组和队列

稀疏数组

案例引入

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

数据结构和算法(Java),上_第1张图片

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

稀疏数组的基本介绍

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

稀疏数组的处理方法:

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

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

实例说明:

数据结构和算法(Java),上_第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.遍历原始的二维数组,得到有效的个数sum
        2.根据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则是随着数据输入而改变,如图所示:

数据结构和算法(Java),上_第3张图片

在这里,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. 将这个数组使用算法,改进成一个环形的队列

数组模拟环形队列

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

数据结构和算法(Java),上_第4张图片

分析说明

模拟循环队列的思路:

  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,其队列的有效数据最大是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 CircleArray{

    private int maxSize; //数组的最大容量
    //front变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素
    private int front; //初始值0
    //rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置.因为希望空出一个空间做为约定
    private int rear; //初始值0
    private 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)介绍

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

数据结构和算法(Java),上_第5张图片

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

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

数据结构和算法(Java),上_第6张图片

单链表的应用实例

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

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

    数据结构和算法(Java),上_第7张图片

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

    思路的分析示意图:

数据结构和算法(Java),上_第8张图片

  1. 修改节点功能

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

  2. 删除节点

    思路分析示意图:

    数据结构和算法(Java),上_第9张图片

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保存head
        HeroNode 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
    @Override
    public 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

数据结构和算法(Java),上_第10张图片

代码实现:

 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是待删除节点的前一个节点

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

数据结构和算法(Java),上_第11张图片

  • 创建:

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

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

代码实现:

public void add(HeroNode heroNode){

            //head节点不能动,定义tail保存head
            HeroNode 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;
        }
    }
  • 插入

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

数据结构和算法(Java),上_第12张图片

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

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

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

  • 删除:

数据结构和算法(Java),上_第13张图片

  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.next
        boolean 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!)

数据结构和算法(Java),上_第14张图片

单向环形链表介绍

数据结构和算法(Java),上_第15张图片

Joseph问题

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

详细介绍:

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

删除节点详细介绍:

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

数据结构和算法(Java),上_第16张图片

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

数据结构和算法(Java),上_第17张图片

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

数据结构和算法(Java),上_第18张图片

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

数据结构和算法(Java),上_第19张图片

以上的思路过程是我根据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向后移动,报数变量加1
                i++;
            }
        }
        System.out.println("第" + q.num + "号人出列");
    }
}
class Person {
    int num;
    Person next;
    public Person(){}
    public Person(int num){
        this.num = num;
    }

    @Override
    public String toString() {
        return "Person{" +
                "num=" + num +
                '}';
    }
}

第4章 栈

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

栈的一个实际需求

请输入一个表达式

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

数据结构和算法(Java),上_第20张图片

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

栈的介绍

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

数据结构和算法(Java),上_第21张图片
数据结构和算法(Java),上_第22张图片

栈的应用场景

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

栈的快速入门

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

数组模拟栈

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

数据结构和算法(Java),上_第23张图片

思路过程:

  • 使用数组来模拟栈
  • 定义一个 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;
    }

    //入栈push
    public 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用来存放单链表头节点的指针,而用头节点指向表的第一个结点,即链栈的栈顶数据元素。这样,确定链表的左端的为栈顶,右端为栈底。下图是链栈的存储结构示意图:

数据结构和算法(Java),上_第24张图片

由于使用带头结点的链表,在对链栈操作的过程中,栈顶指针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;
    }

    @Override
    public 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 –

使用栈来实现综合计算器

思路分析:

数据结构和算法(Java),上_第25张图片

代码实现:

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; //用于保存每次扫描得到的char
        String 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;
    }

    //入栈push
    public 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]);
        }
    }

    //增加一个方法,可以返回当前栈顶的值,但是不是真正的pop
    public 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=>164
        String suffixExpression = "4 5 * 8 - 60 + 8 2 / +"; //4*5-8+60+8/2=76
        List<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=>后缀表达式对应的List
    public static List<String> parseSuffixExpressionList(List<String> ls) {
        //定义两个栈
        Stack<String> s1 = new Stack<>(); //符号栈
        //说明:因为s2这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
        //因此比较麻烦,这里我们就不用Stack直接使用Lists2
        List<String> s2 = new ArrayList<>(); //存贮中间结果的list
        for (String item : ls) {
            if(item.matches("\\d+")){
                //是数字,就加入到s2
                s2.add(item);
            }else if("(".equals(item)){
                //如果是左括号“(”,则直接压入s1
                s1.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中剩余的运算符依次弹出并加入s2
        while (s1.size() != 0){
            s2.add(s1.pop());
        }
        return s2;
        //注意因为是存放到List,因此按顺序输出就是对应的后缀表达式对应的List
    }

    //将中缀表达式转成对应的list
    public static List<String> toInfixExpressionList(String s) {
        List<String> list = new ArrayList<String>();
        String str; //字符串拼接
        int i = 0;
        char c;
        do{
            //如果是非数字,直接加入到list
            if((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)

数据结构和算法(Java),上_第26张图片

递归的概念

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

递归调用机制

数据结构和算法(Java),上_第27张图片

递归能解决什么样的问题

  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 表示墙
		// 上下全部置为1
		for (int i = 0; i < 7; i++) {
			map[0][i] = 1;
			map[7][i] = 1;
		}

		// 左右全部置为1
		for (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格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

数据结构和算法(Java),上_第28张图片

八皇后问题思路分析

用回溯的思想解决:

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

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

详细思路:

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

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

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

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

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

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

  6. 实例:

数据结构和算法(Java),上_第29张图片

八皇后问题代码实现

说明:

理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题.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. 常见的排序算法分类:

数据结构和算法(Java),上_第30张图片

算法的时间复杂度

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

  • 事后统计的方法

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

  • 事前估算的方法

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

时间频度

基本介绍:

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

  1. 举例说明-基本案例

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

    T(n)=n+1;
    数据结构和算法(Java),上_第31张图片

    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

数据结构和算法(Java),上_第32张图片

结论:

  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

数据结构和算法(Java),上_第33张图片

结论:

  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

数据结构和算法(Java),上_第34张图片

结论:

  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)

数据结构和算法(Java),上_第35张图片

说明:

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

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

数据结构和算法(Java),上_第36张图片

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

  1. 对数阶O(log2n)

数据结构和算法(Java),上_第37张图片

说明:在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)

数据结构和算法(Java),上_第38张图片

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

  1. 线性对数阶O(nlogN)

    数据结构和算法(Java),上_第39张图片

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

  2. 平方阶O(n²)

数据结构和算法(Java),上_第40张图片

说明:平方阶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判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,再进行)

过程图解

数据结构和算法(Java),上_第41张图片

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

若有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次选择操作。(将最小的数据放到最前方)。

过程图解

数据结构和算法(Java),上_第42张图片

稳定性:选择排序不稳定:举个例子,序列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个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

过程图解

数据结构和算法(Java),上_第43张图片

稳定性:插入排序稳定!

应用实例

代码实现:

 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,所以要加1
            if(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,所以要加1
            if(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时,整个文件恰被分成一组,算法便终止。

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

过程图解

数据结构和算法(Java),上_第44张图片

注意:

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

数据结构和算法(Java),上_第45张图片

稳定性:希尔排序不稳定:一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以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)是对冒泡排序的一种改进。基本思想是:通过一轮的排序将序列分割成独立的两部分,其中一部分序列的关键字(这里主要用值来表示)均比另一部分关键字小。继续对长度较短的序列进行同样的分割,最后到达整体有序。在排序过程中,由于已经分开的两部分的元素不需要进行比较,故减少了比较次数,降低了排序时间。

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

过程图解

数据结构和算法(Java),上_第46张图片

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

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

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

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

  • 左右指针法

序列: 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。
    数据结构和算法(Java),上_第47张图片

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

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

数据结构和算法(Java),上_第49张图片

  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

数据结构和算法(Java),上_第50张图片

  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重复在快排中其中有一个一定是基准,因此假如进行一次比较之后就不会再次进行比较了。
    • 第二,假如我们已知 a冗余,而在快排中这种情况是不会发生的,因为 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个数据耗时:97ms
        int[] 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-基本思想:

数据结构和算法(Java),上_第51张图片

归并排序思想示意图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个数据耗时:107ms
        int[] 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年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基数排序的基本思想

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

过程图解

数据结构和算法(Java),上_第52张图片
数据结构和算法(Java),上_第53张图片
数据结构和算法(Java),上_第54张图片

稳定性:基数排序稳定

基数排序的说明

  • 基数排序是对传统桶排序的扩展,速度很快.
  • 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 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个桶中的哪个位置,初始值是0
                bucket[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个数据耗时:22ms
        int[] 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),上_第55张图片

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

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

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

稳定与不稳定的具体分析参考博文:

第7章 查找算法

查找算法介绍

在java中,我们常用的查找有四种:

  • 顺序(线性)查找
  • 二分查找/折半查找
  • 插值查找
  • 斐波那契查找

线性查找算法

代码实现:

package com.atguigu.search;

public class SeqSearch {

	public static void main(String[] args) {
		int arr[] = { 1, 9, 11, -1, 34, 89 };// 没有顺序的数组
		int index = seqSearch(arr, -11);
		if(index == -1) {
			System.out.println("没有找到到");
		} else {
			System.out.println("找到,下标为=" + index);
		}
	}

	/**
	 * 这里我们实现的线性查找是找到一个满足条件的值,就返回
	 * @param arr
	 * @param value
	 * @return
	 */
	public static int seqSearch(int[] arr, int value) {
		// 线性查找是逐一比对,发现有相同值,就返回下标
		for (int i = 0; i < arr.length; i++) {
			if(arr[i] == value) {
				return i;
			}
		}
		return -1;
	}
}

二分查找算法

思路过程

  1. 首先确定该数组的中间的下标

    mid = (left + right) / 2

  2. 然后让需要查找的数 findVal 和 arr[mid] 比较

    • findVal > arr[mid] , 说明你要查找的数在mid 的右边, 因此需要递归的向右查找
    • findVal < arr[mid], 说明你要查找的数在mid 的左边, 因此需要递归的向左查找
    • findVal == arr[mid] 说明找到,就返回

递归结束的条件:

  • 找到就结束递归
  • 递归完整个数组,仍然没有找到findVal ,也需要结束递归 当 left > right 就需要退出

二分查找的代码:

package cn.ysk.search;

import java.util.ArrayList;

public class BinarySearch {
    public static void main(String[] args) {
        int[] arr = {1,2,3,5,7,8,9};
        int value = 8;
        int index = binarySearch(arr, 0, arr.length -1, value);
        System.out.println(index);
//        ArrayList list = binarySearch2(arr, 0, arr.length - 1, value);
//        System.out.println(list);
    }


    /**
     *
     * @param arr 数组
     * @param left  左边的索引
     * @param right 右边的索引
     * @param value 要查找的值
     * @return      找到后,返回其所在的下标,否则,返回-1
     */
    public static int binarySearch(int arr[],int left,int right,int value) {
        //当 left > right 时,说明递归整个数组,但是没有找到
        if (left > right) {
            return -1;
        }
        int mid = (left + right) / 2;
        int midVal = arr[mid];

        if (value > midVal) { // 向 右递归
            return binarySearch(arr, mid + 1, right, value);
        } else if (value < midVal) { // 向左递归
            return binarySearch(arr, left, mid - 1, value);
        } else {
            return mid;
        }

    }

    //完成一个课后思考题:
    /*
     * 课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,
     * 有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000
     *
     * 思路分析
     * 1. 在找到mid 索引值,不要马上返回
     * 2. 向mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList
     * 3. 向mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList
     * 4. 将Arraylist返回
     */
    public static ArrayList<Integer> binarySearch2(int arr[], int left, int right, int value) {
            if(left>right){
                return new ArrayList<>();
            }
            int mid = (left+right)/2;
            if(value < arr[mid]) {
                return binarySearch2(arr, left, mid-1, value);
            }else if(value >arr[mid]) {
                return binarySearch2(arr, mid+1, right, value);
            }else {
                //			 * 思路分析
//			 * 1. 在找到mid 索引值,不要马上返回
//			 * 2. 向mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList
//			 * 3. 向mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList
//			 * 4. 将Arraylist返回
                ArrayList<Integer> list = new ArrayList<>();
                int temp = mid -1;
                while (true) { //向左边找,因为本身是有序的,若找到一个不为value的值就直接退出
                    if(temp < 0 || arr[temp] != value) {
                        break;
                    }
                    list.add(temp);
                    temp--;
                }
                list.add(mid);
                temp = mid + 1;
                while (true) { //向右边找
                    if(temp > arr.length-1 || arr[temp] != value) {
                        break;
                    }
                    list.add(temp);
                    temp++;
                }
                return list;
            }
    }
}

插值查找算法

原理介绍

  1. 插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找

  2. 将折半查找中的求mid 索引的公式 , low 表示左边索引left, high表示右边索引right.key 就是要查找的值。

    可看成key在整序列中的占比哟

在这里插入图片描述

  1. int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;/ *插值索引 */对应前面的代码公式:

    int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])

注意事项

  1. 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
  2. 关键字分布不均匀的情况下,该方法不一定比折半查找要好

插值查找代码

代码实现:

package cn.ysk.search;

public class InsertValueSearch {
    public static void main(String[] args) {
        		int [] arr = new int[100];
		for(int i = 0; i < 100; i++) {
			arr[i] = i + 1;
		}

//        int arr[] = { 1, 8, 10, 89,1000,1000, 1234 , 1235, 1237};

        int index = insertValueSearch(arr, 0, arr.length - 1, 51);
        System.out.println(index);
    }

    public static int insertValueSearch(int arr[],int left,int right,int value) {
        System.out.println("插入查找一次~");
        //注意:findVal < arr[0]  和  findVal > arr[arr.length - 1] 必须需要
        //否则我们得到的 mid 可能越界
        if(left > right || value < arr[0] || value > arr[arr.length-1]) {
            return -1;
        }
        int mid = left + (right - left)*(value - arr[left])/(arr[right]-arr[left]);
        if(value > arr[mid]) {
            return insertValueSearch(arr, mid+1, right, value);
        }else if (value < arr[mid]) {
            return insertValueSearch(arr, left, mid-1, value);
        }else {
            return mid;
        }
    }
}

斐波拉契(黄金分割法)查找算法

基本介绍

黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意想不到的效果。

查找原理

斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近,即mid=low+F(k-1)-1(F代表斐波那契数列),如下图所示:

数据结构和算法(Java),上_第56张图片

对F(k-1)-1的理解:

  1. 由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1 。该式说明:只要顺序表的长度为F[k]-1,则可以将该表分成长度为F[k-1]-1和F[k-2]-1的两段,即如上图所示。从而中间位置为mid=low+F(k-1)-1

  2. 类似的,每一子段也可以用相同的方式分割

  3. 但顺序表长度n不一定刚好等于F[k]-1,所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能使得F[k]-1恰好大于或等于n即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1到F[k]-1位置),都赋为n位置的值即可。

    策略是采用“大于数组长度的最近一个斐波那契数值”。比如当前数组长度为25,斐波那契数列中大于25的最近元素为34。

     while (high > f[k] - 1) {
                k++;
     }
    

    两篇博文:

斐波拉契查找的代码

package cn.ysk.search;

import java.util.Arrays;

public class FibonacciSearch {
    public static int maxSize = 20;

    public static void main(String[] args) {
        int[] arr = {1, 8, 10, 89, 1000, 1234};

        System.out.println("index=" + fibSearch(arr, 1234));// 0
    }

    public static int[] fib() {
        int[] f = new int[maxSize];
        f[0] = 1;
        f[1] = 1;
        for (int i = 2; i < maxSize; i++) {  //构造一个斐波拉契数列
            f[i] = f[i - 1] + f[i - 2];
        }
        return f;
    }

    /**
     * 采用非递归的方式编写
     *
     * @param arr
     * @param key
     * @return 成功查找之后的索引
     */
    public static int fibSearch(int arr[], int key) {
        int low = 0;
        int mid = 0;
        int high = arr.length - 1;
        int[] f = fib();
        int k = 0;
        //获取到斐波那契分割数值的下标,策略是采用“大于数组长度的最近一个斐波那契数值”
        while (high > f[k] - 1) {
            k++;
        }
//        首先进行复制,因为填充过程会更改原始数据;
//        copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值(会使用0填充)
        int[] temp = Arrays.copyOf(arr, f[k]);
        //实际上需求使用a数组最后的数填充 temp
        //temp = {1,8, 10, 89, 1000, 1234, 0, 0}  => {1,8, 10, 89, 1000, 1234, 1234, 1234,}
        for (int i = high + 1; i < temp.length; i++) {
            temp[i] = arr[high];
        }
        while (low <= high) { // 只要这个条件满足,就可以找
            mid = low + f[k - 1] - 1;
            if (key < temp[mid]) { //我们应该继续向数组的前面查找(左边)
                high = mid - 1;
                //为甚是 k--
                //说明
                //1. 全部元素 = 前面的元素 + 后边元素
                //2. f[k] = f[k-1] + f[k-2]
                //因为 前面有 f[k-1]个元素,所以可以继续拆分 f[k-1] = f[k-2] + f[k-3]
                //即 在 f[k-1] 的前面继续查找 k--
                //即下次循环 mid = f[k-1-1]-1
                k--;
            } else if (key > temp[mid]) { // 我们应该继续向数组的后面查找(右边)
                low = mid + 1;
                //为什么是k -=2
                //说明
                //1. 全部元素 = 前面的元素 + 后边元素
                //2. f[k] = f[k-1] + f[k-2]
                //3. 因为后面我们有f[k-2] 所以可以继续拆分 f[k-1] = f[k-3] + f[k-4]
                //4. 即在f[k-2] 的前面进行查找 k -=2
                //5. 即下次循环 mid = f[k - 1 - 2] - 1
                k -= 2;
            } else { //找到
                //需要确定,返回的是哪个下标
                if (mid <= high) {
                    return mid;
                } else {
                    return high;
                }
            }
        }
        return -1;
    }
}

第8章 哈希表

哈希表(散列)-Google上机题

  1. 看一个实际需求,google公司的一个上机题:
  2. 有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址…),当输入该员工的id时,要求查找到该员工的所有信息
  3. 要求:不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)

哈希表的基本介绍

散列表(Hashtable,也叫哈希表),是根据关键码值(Keyvalue)而直接进行访问的数据结构(例如:字典)。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

数据结构和算法(Java),上_第57张图片

什么是哈希表?

哈希表简单来说可以看作是是对数组的升级,(也有不少人认为哈希表的本质就是数组)。

我们在利用数组存储数据的时候,记录在数组中的位置是随机的,位置和记录的关键字之间不存在确定的关系。

联系:哈希表是由数组实现的。

区别:数组中存储的元素的和数组下标没有确定的关系,而哈希表中存储的元素和数组的下标有一个确定的关系,我们将这个确定的关系称之为哈希函数(Hash).

当我们用一个确定的关系即哈希函数(Hash)来确定关键字和记录的存储位置时,换句话说确定(例如:学号)和数组下标的对应关系。那么这个数组就可称之为哈希表

其中一种最常见的方法是链地址法:建一个元素为链表的数组,数组大小为哈希函数产生的哈希地址的区间,每个数组元素初始都为空,所有地址冲突的关键字存放在同一个链表里(但凡是哈希地址为i的记录都插在S[i]这个元素后面)。

哈希表的总结

理论上来讲哈希表访问某一个记录的时间开销是O(1),直接通过哈希函数和关键字找到记录的存储地址,但是由于冲突的产生,使得哈希表的查找过程仍然需要关键字去和一个哈希表中的元素来进行比较。也就是说哈希表尽可能地减少我们要比较的次数,如果能够经过计算查询到我们要访问的记录那是最理想的情况。但是由于冲突导致我们还需要进行“比较”。

打个比方:

这就像我们去利用字典查找汉字,我们虽然能够利用汉字的首字母或者汉子的笔画去确定该汉字在字典中的页码,但是该页码中还存在其他的汉字,我们需要将查找的汉字和该页码中的汉字进行比较,直到找到我们要查找的汉字。在利用字典查找汉字的过程中,我们虽然仍然要去比较汉字,但相比与在所有页码中去一个个比较查询,这种先确定页码再去比较的方案已经能极大地提高我们查找的效率了。我想这就是哈希表存在的意义吧。

上述过程引用于:

google公司的一个上机题:

有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,名字,住址…),当输入该员工的id时,要求查找到该员工的所有信息。

要求:

  1. 不使用数据库,速度越快越好=>哈希表(散列

  2. 添加时,保证按照id从低到高插入[课后思考:如果id不是从低到高插入,但要求各条链表仍是从低到高,怎么解决?]

  3. 使用链表来实现哈希表,该链表不带表头[即:链表的第一个结点就存放雇员信息]

  4. 思路分析并画出示意图

数据结构和算法(Java),上_第58张图片

代码实现:

package cn.ysk.hashtab;

import java.util.Scanner;

public class HashTabDemo {
    public static void main(String[] args) {
        HashTab hashTab = new HashTab(7);
        String key ="";
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("add:  添加雇员");
            System.out.println("list: 显示雇员");
            System.out.println("find: 查找雇员");
            System.out.println("exit: 退出系统");
            System.out.println("请输入您要进行的操作:");
            key = scanner.next();
            switch (key) {
                case "add" :
                    System.out.println("id:");
                    int id = scanner.nextInt();
                    System.out.println("name:");
                    String name = scanner.next();//这里不能用nextLine,不然它会读取空格,导致没有输入就直接向下进行
                    Emp emp = new Emp(id,name);
                    hashTab.add(emp);
                    break;
                case "list" :
                    hashTab.list();
                    break;
                case "find" :
                    System.out.println("请输入要查找的id:");
                    id = scanner.nextInt();
                    hashTab.findEmpById(id);
                    break;
                case "exit" :
                    scanner.close();
                    System.exit(0);
                default:
                    break;
            }
        }
    }

}
//管理多条链表
class HashTab {
    private EmpLinkedList[] empLinkedListArray;
    private int size;  //多少条链表

    public HashTab(int size) {
        this.size = size;  //后面会用到
        empLinkedListArray = new EmpLinkedList[size];
        //数组中的每个元素还得初始化,要不然全为null
        for (int i = 0; i < size; i++) {
            empLinkedListArray[i] = new EmpLinkedList();
        }
    }

    //添加
    public void add(Emp emp) {
        int empLinkedListNO = hashFun(emp.id);
        empLinkedListArray[empLinkedListNO].add(emp);
    }

    //遍历
    public void list() {
        for (int i = 0; i < size; i++) {
            empLinkedListArray[i].list(i);
        }
    }

    //查找
    public void findEmpById(int id) {
        int empLinkedListNo = hashFun(id);
        Emp emp = empLinkedListArray[empLinkedListNo].findEmpById(id);
        if(emp != null) {
            System.out.println("第"+empLinkedListNo+"条链表中找到id为"+id+"的雇员!");
        }else {
            System.out.println("哈希表中没有此雇员~!");
        }
    }
    //散列函数
    public int hashFun(int id) {
        return id % size;
    }
}
class Emp {
    public int id;
    public String name;
    public Emp next;

    public Emp(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
class EmpLinkedList {
    //在这里创建的是不带头结点的链表
    private Emp head;

    //添加雇员到链表
    //说明
    //1. 假定,当添加雇员时,id 是自增长,即id的分配总是从小到大
    //   因此我们将该雇员直接加入到本链表的最后即可
    public void add(Emp emp) {
        //如果是第一个雇员
        if(head == null) {
            head = emp;
            return;
        }
        //不是第一个雇员
        Emp curEmp = head;
        while (true) {  //找到链表的最后
            if(curEmp.next == null) {
                break;
            }
            curEmp = curEmp.next;
        }
        curEmp.next = emp;
    }

    public void list(int no) {
        if(head == null) {
            System.out.println("第"+(no+1)+"链表为空");
            return;
        }
        System.out.print("第"+(no+1)+"链表的信息为:");
        Emp curEmp = head;
        while (true) {
            System.out.printf("id=%d,name=%s\t", curEmp.id,curEmp.name);
            if(curEmp.next == null) {
                break;
            }
            curEmp = curEmp.next;
        }
        System.out.println();
    }

    public Emp findEmpById(int id) {
        if(head == null) {
            System.out.println("链表为空!");
            return null;
        }
        Emp curEmp = head;
        while (true) {
            if(curEmp.id == id) {
                break;
            }
            if(curEmp.next == null) {
                //证明找到最后都没找到
                curEmp = null;
            }
            curEmp = curEmp.next;
        }
        return curEmp;
    }

}

你可能感兴趣的:(leetcode,数据结构,算法,java)