java 数据结构和算法

1、数据结构和算法的概述

1.1 数据结构和算法的重要性

  • 算法是程序的灵魂。优秀的程序可以在海量数据计算时,依然保持高速运算。
  • 一般来讲程序会使用内存计算框架(比如Spark)和缓存技术(比如Redis等)来优化程序,再深入的思考一下,这些季孙框架和缓存技 术,它的核心功能是哪部分呢?
  • 拿实际工作经历来说,在Unix下开发服务器程序,功能是要支持上千万人同时在线,在上线前,做内测,一切🆗,可上线后,服务器就支撑不住了,公司的CTO对代码进行优化,再次上线,坚如磐石。你就能感受到程序是有灵魂的,就是算法。
  • 目前程序员面试的门槛越来越高,很多一线IT功能(大厂),都会有数据结构和算法面试题(负责的告诉你,肯定有的)
  • 如果你不想永远都是代码工人,那就花时间来研究下数据结构和算法。

1.2 数据结构和算法的关系

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

2、数据结构——线性结构和非线性结构

  • 线性结构

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

    • 非线性结构包括:二维数组、 多维数组、广义表、树结构、图结构。

2.1 稀疏sparsearray数组

  • 先看一个实际的需求

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

    分析问题:因为该二维数组的很多值是默认值0,因此记录了很多没有意义的数据 --> 稀疏数组

  • 基本介绍
  • 当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保护该数组。
  • 稀疏数组的处理方法是:
    1)记录一个数组一共有几行几列,有多少个不同的值
    2)把具有不同值的元素的行列及值记录在一个小规模的数组中,从小缩小程序的规模

2.2 队列

2.2.1 队列介绍

- 队列是一个有序列表,可以使用数组或链表来实现
- 遵循先入先出的原则。即,先存入队列的数据,要先取出。后存入的数据后取出
- 示意图
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/02ea826ad5564a18bcaf4ce7d64384bd.png)

2.2.2 队列的使用场景 — 银行排队的案例

在这里插入图片描述

2.2.3 数组模拟队列的思路

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

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

  • 当我们将数据存入队列时称为 “addQueue”, addQueue处理需要两个步骤:思路分析
    1)将尾针指针往后移动: rear + 1, 当front = = rear 【空】
    2)若尾针指针rear 小于队列的最大下标 maxSize - 1,则将数据存入rear 所指的数组元素中,否则无法存入数据。 rear == maxSize - 1 【队列满】

  • 代码实现

    package com.sss.quene;
    
    import java.util.Scanner;
    
     public class ArrayQueneDemo {
         public static void main(String[] args) {
             // 测试一把
             // 创建一个队列
             ArrayQueue queue = new ArrayQueue(3);
             char key = ' '; // 接收用户输入
             Scanner scanner = new Scanner(System.in);
             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): 查看队列头的数据");
                 key = scanner.next().charAt(0); // 接收一个字符
                 switch (key) {
                     case 's':
                         queue.showQueue();
                         break;
                     case 'a':
                         System.out.println("输入一个数");
                         int value = scanner.nextInt();
                         queue.addQueue(value);
                         break;
                     case 'g': // 取出数据
                         try {
                             int res = queue.getQueue();
                             System.out.printf("取出的数据是%d\n", res);
                         } catch (Exception e) {
                             System.out.println(e.getMessage());
                         }
                         break;
                     case 'h':
                         try {
                             int head = queue.headQueue();
                             System.out.printf("队列头的数据是%d\n", head);
                         } catch (Exception e) {
                             System.out.println(e.getMessage());
                         }
                         break;
                     case 'e': // 退出
                         scanner.close();
                         loop = false;
                         break;
                     default:
                         break;
                 }
             }
             System.out.println("程序退出");
         }
     
     }
     
     class  ArrayQueue {
     
         private int maxSize;//表示数组的最大容量
         private int front;//队列头
         private int rear;//队列尾
         private int[] arr;//该数据用于存放数据,模拟队列
     
     
         // 创建队列的构造器
         public ArrayQueue(int maxSize) {
             this.maxSize = maxSize;
             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\n", i, arr[i]);
             }
         }
     
         // 显示队列的头数据,注意不是取出数据
         public int headQueue() {
             // 判断
             if(isEmpty()) {
                 throw new RuntimeException("队列空,没有数据");
             }
             return arr[front + 1];
         }
     
     }
    
    
  • 问题分析及优化
    1)目前数组使用一次就不能使用,没有达到复用的效果
    2)将这个数组使用算法,改进成一个环形的队列 取模: %

2.2.4 数组模拟环形队列

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

  • 分析说明
    • 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在判断队列满的时候需要注意 (rear + 1) % maxSize == front 满
    • rear == front [空]
    • 分析示意图:
      在这里插入图片描述
      思路如下:
      1、front变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front] 就是队列的第一个元素 front的初始值为 0
      2、rear变量的含义做一个调整:rear指向队列最后一个元素的后一个位置。因为希望空出一个空间作为约定,rear的初始值为 0
      3、当队列满时,条件是(rear + 1)%maxSize = front [满]
      4、队列为空的条件, rear == front
      5、当我们这样分析,队列中有效的数据的个数为 (rear + maxSize - front)% maxSize // rear = 1 front = 0
      6、我们就可以在原来的队列上

代码实现

public class CircleArrayQueueDemo {

   public static void main(String[] args) {
       // 测试一把
       System.out.println("测试数组模拟环形队列的案例··");

       // 创建一个队列
       CircleArrayQueue queue = new CircleArrayQueue(4);// 说明设置4, 其队列的有效数据最大是3
       char key = ' '; // 接收用户输入
       Scanner scanner = new Scanner(System.in);
       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): 查看队列头的数据");
           key = scanner.next().charAt(0); // 接收一个字符
           switch (key) {
               case 's':
                   queue.showQueue();
                   break;
               case 'a':
                   System.out.println("输入一个数");
                   int value = scanner.nextInt();
                   queue.addQueue(value);
                   break;
               case 'g': // 取出数据
                   try {
                       int res = queue.getQueue();
                       System.out.printf("取出的数据是%d\n", res);
                   } catch (Exception e) {
                       System.out.println(e.getMessage());
                   }
                   break;
               case 'h':
                   try {
                       int head = queue.headQueue();
                       System.out.printf("队列头的数据是%d\n", head);
                   } catch (Exception e) {
                       System.out.println(e.getMessage());
                   }
                   break;
               case 'e': // 退出
                   scanner.close();
                   loop = false;
                   break;
               default:
                   break;
           }
       }
       System.out.println("程序退出");
   }
}

class CircleArrayQueue {

   private int maxSize;//表示数组的最大容量
   private int front;// front就指向队列的第一个元素,也就是说arr[front] 就是队列的第一个元素 front的初始值为 0
   private int rear;// rear指向队列最后一个元素的后一个位置。因为希望空出一个空间作为约定,rear的初始值为 0
   private int[] arr;//该数据用于存放数据,模拟队列

   public CircleArrayQueue(int maxSize) {
       this.maxSize = maxSize;
       arr = new int[maxSize];
   }

   // 判断队列是否满
   public boolean isFull() {
       return rear == maxSize - 1;
   }

   // 判断队列是否为空
   public boolean isEmpty() {
       return (rear + 1)%maxSize == front;
   }

   // 添加数据到队列
   public void addQueue(int n) {
       // 判断数据是否满
       if(isFull()) {
           System.out.println("队列满,不能添加数据");
           return;
       }
       arr[rear] = n;
       // 将rear后移,这里必须考虑取模
       rear = (rear + 1) % maxSize;
   }

   public int getQueue() {
       // 判断队列是否空
       if(isEmpty()) {
           throw new RuntimeException("队列空,不能取出数据");
       }
       // 这里需要分析出front是指向队列的第一个元素
       // 1、先把front对应的值保留到一个额临时变量
       // 2、将front后移,考虑取模
       // 3、将临时保存的变量返回
       int value = arr[front];
       front = (front + 1) % maxSize;
       return value;
   }

   // 显示队列的所有数据
   public void showQueue() {
       // 遍历
       if(isEmpty()) {
           System.out.println("队列空,没有数据");
           return;
       }
       // 思路:从front开始遍历,遍历多少个元素
       for (int i = front; i < front + size(); i++) {
           System.out.printf("arr[%d]=%d\n", i % maxSize, arr[i % maxSize]);
       }
   }

   // 求出当前队列有效数据的个数
   public int size() {
       return (rear + maxSize - front) % maxSize;
   }

   // 显示队列的头数据,注意不是取出数据
   public int headQueue() {
       // 判断
       if(isEmpty()) {
           throw new RuntimeException("队列空,没有数据");
       }
       return arr[front];
   }
}

2.3 链表

2.3.1 链表介绍

链表是有序的列表,但他们在内存中的存储如下:在这里插入图片描述
小结

  • 链表是以节点的方式来存储,是链式存储
  • 每个节点包含data域、next域:指向下一个节点
  • 如图:发现链表的各个节点不一定是连续存储
  • 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
public class SingleLinkedListDemo {
    public static void main(String[] args) {
        // 测试一把
        HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星");
        HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头");

        // 创建要给链表
        SingleLinkedList singleLinkedList = new SingleLinkedList();
        // 加入链表尾部
//        singleLinkedList.add(heroNode1);
//        singleLinkedList.add(heroNode2);
//        singleLinkedList.add(heroNode3);
//        singleLinkedList.add(heroNode4);

        // 加入 按照编号的顺序
        singleLinkedList.addByOrder(heroNode1);
        singleLinkedList.addByOrder(heroNode4);
        singleLinkedList.addByOrder(heroNode2);
        singleLinkedList.addByOrder(heroNode3);

        // 显示一把
        singleLinkedList.list();

        // 测试修改节点的代码
        HeroNode newHeroNode = new HeroNode(2, "小卢","玉麒麟~~");
        singleLinkedList.update(newHeroNode);

        // 显示一把
        singleLinkedList.list();

        singleLinkedList.del(1);

        System.out.println("删除后的链表");

        singleLinkedList.list();

    }

}

// 定义SingleLinkedList 管理我们的英雄
class SingleLinkedList {
    // 先初始化一个头节点 头节点不要动 不存放具体数据
    HeroNode head = new HeroNode(0, "", "");
    // 添加节点到单向链表
    // 思路:当不考虑编号顺序时  1、找到当前链表的最后节点 2、将最后这个节点的next 指向新的节点
    public void add(HeroNode heroNode) {
        // 因为head节点不能动,因此我们需要一个辅助遍历 temp
        HeroNode temp = head;
        // 遍历链表,找到最后
        while(true) {
            // 找到链表的最后
            if(temp.next == null) {
                break;
            }
            // 如果没有找到最后,就将temp后移
            temp = temp.next;
        }
        // 当退出while循环时,temp就指向了链表的最后
        // 将最后这个节点的next指向 新的节点
        temp.next = heroNode;
    }

    // 第二种方式在添加英雄时,根据排名将英雄添加到指定位置
    //(如果有这个排名,则添加失败,将这个信息提示出来)
    public void addByOrder(HeroNode heroNode) {
        // 因为头节点不能动 因此我们仍然通过一个辅助(指针)找到添加的位置
        // 因为单链表 因为我们找的temp 是位于 添加位置的前一个节点 否则插入不了
        HeroNode temp = head;
        boolean flag = false; // flag标志添加的编号是否存在,默认为false
        while (true) {
            if(temp.next == null) { // 说明temp已经在链表的最后
                break;
            }
            if(temp.next.no > heroNode.no) { // 位置找到 就在temp的后面插入
                break;
            } else if (temp.next.no == heroNode.no) {
                flag = true; // 说明编号存在
                break;
            }
            temp = temp.next; // 后移 遍历当前链表
        }
        // 判断flag的值
        if(flag) { // 不能添加 说明编号存在
            System.out.printf("准备插入的英雄的编号 %d 已经存在,不能加入 \n", heroNode.no);
        } else {
            // 插入到链表中, temp后面
            heroNode.next = temp.next;
            temp.next = heroNode;
        }
    }

    // 修改节点的信息  根据 no编号来修改 即 no编号不能改
    // 说明: 根据 newHeroNode 的 no 来修改即可
    public void update(HeroNode newHeroNode) {
        // 判断是否空
        if(head.next == null) {
            System.out.println("链表为空~");
            return;
        }
        // 找到需要修改的节点,根据no编号
        // 定义一个辅助变量
        HeroNode temp = head.next;
        boolean flag = false; // 表示是否找到该节点
        while (true) {
            if(temp == null) {
                break;// 已经遍历完链表
            }
            if(temp.no == newHeroNode.no) {
                // 找到
                flag = true;
                break;
            }
            temp = temp.next;
        }
        //根据flag 判断是否找到要修改的节点
        if(flag) {
            temp.name = newHeroNode.name;
            temp.nickname = newHeroNode.nickname;
        } else { // 没有找到
            System.out.printf("没有找到编号 %d 的节点,不能修改\n", newHeroNode.no);
        }
    }

    // 删除节点
    // 思路:1、head不能动, 因此我们需要一个辅助变量temp 找到待删除节点的前一个节点
    // 2、说明 我们在比较时,是temp.next.no 和 需要删除节点的 no 比较
    public void del(int no) {
        HeroNode temp = head;
        boolean flag = false; // 标志是否找到待删除节点的
        while (true) {
            if(temp.next == null) { // 已经走到链表最后
                break;
            }
            if(temp.next.no == no) {
                // 找到待删除节点的前一个节点temp
                flag = true;
                break;
            }
            temp = temp.next; // temp后移, 遍历
        }
        // 判断flag
        if(flag) { // 找到
            // 可以删除
            temp.next = temp.next.next;
        } else {
            System.out.printf("要删除的节点 %d 节点不存在\n", no);
        }
    }

    // 显示链表【遍历】
    public void list() {
        // 判断链表是否为空
        if(head.next == null) {
            System.out.println("链表为空");
            return;
        }
        // 因为头节点不能动 我们需要一个辅助变量来变量
        HeroNode temp = head.next;
        while (true) {
            // 判断是否到链表最后
            if(temp == null) {
                break;
            }
            // 输出节点的信息
            System.out.println(temp);
            // 将temp后移  一定小心
            temp = temp.next;
        }
    }

    // 将单链表反转
    public static void reversetList(HeroNode head) {
        // 如果当前链表为空 或者只有一个节点 则无需反转  直接返回
        if(head.next == null || head.next.next == null) {
            return;
        }
        // 定义一个辅助的指针(变量),帮助我们遍历原来的链表
        HeroNode cur = head.next;
        HeroNode next = null;// 指向当前节点的 [cur] 下一个节点
        HeroNode reverseHead = new HeroNode(0, "", "");
        // 遍历原来的链表 每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
        while (cur != null) {
            next = cur.next;// 先暂时保存当前节点的下一个节点,因为后面要使用
            cur.next = reverseHead.next; // 将cur的下一个节点指向新链表的最前端
            reverseHead.next = cur;// 将cur 连接到新的链表上
            cur = next;//让cur后移
        }
        // 将head.next 指向reverseHead.next 实现单链表的反转
        head.next = reverseHead.next;
    }

    // 从尾到头打印单链表
    // 可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
    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;// cur 后移,这样就可以压入下一个节点
        }
        // 将栈中的节点进行打印,pop出栈
        while (stack.size() > 0) {
            System.out.println(stack.pop()); // stack的特点是先进后出
        }
    }
}

// 定义HeroNode,每个HeroNode 对象就是一个节点
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 + '\'' +
                '}';
    }
}

2.3.2 双向链表

  • 单向链表,查找的方向只能是一个方向,而双向链表可以向前或向后查找。

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

  • 双向链表的遍历、添加、修改、删除操作思路 ==》 代码实现

    • 遍历方式和单链表一样,只是可以向前,也可以向后查找
    • 添加(默认添加到双向链表的最后)
      1、先找到双向链表的最后一个节点
      2、temp.next = newHeroNode;
      3、newHeroNode.pre = temp;
    • 修改思路和原理 和单链表一样
    • 删除
      1、因为是双向链表,因此我们可以实现自我删除某个节点
      2、直接找到要删除的这个节点,比如temp
      3、temp.pre.next = temp.next;
      4、temp.next.pre = temp.pre;
public class DoubleLinkedListDemo {
   public static void main(String[] args) {
       // 测试
       System.out.println("双向链表的测试");
       // 先创建节点
       HeroNode2 heroNode1 = new HeroNode2(1, "宋江", "及时雨");
       HeroNode2 heroNode2 = new HeroNode2(2, "卢俊义", "玉麒麟");
       HeroNode2 heroNode3 = new HeroNode2(3, "吴用", "智多星");
       HeroNode2 heroNode4 = new HeroNode2(4, "林冲", "豹子头");

       // 创建一个双向链表
       DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
       doubleLinkedList.add(heroNode1);
       doubleLinkedList.add(heroNode2);
       doubleLinkedList.add(heroNode3);
       doubleLinkedList.add(heroNode4);

       doubleLinkedList.list();

       // 修改
       HeroNode2 newHeroNode = new HeroNode2(4,"公孙策","入云龙");
       doubleLinkedList.update(newHeroNode);
       System.out.println("修改后的链表情况");
       doubleLinkedList.list();

       // 删除
       doubleLinkedList.del(4);
       System.out.println("删除后的链表情况");
       doubleLinkedList.list();
   }
}

// 创建一个双向链表的类
class DoubleLinkedList {
   // 先创建一个头节点,头节点不要动,不存放具体的数据
   HeroNode2 head = new HeroNode2(0,"","");

   // 返回头节点
   public HeroNode2 getHead() {
       return head;
   }

   // 变量双向链表的方法
   // 显示链表【遍历】
   public void list() {
       // 判断链表是否为空
       if(head.next == null) {
           System.out.println("链表为空");
           return;
       }
       // 因为头节点不能动 我们需要一个辅助变量来变量
       HeroNode2 temp = head.next;
       while (true) {
           // 判断是否到链表最后
           if(temp == null) {
               break;
           }
           // 输出节点的信息
           System.out.println(temp);
           // 将temp后移  一定小心
           temp = temp.next;
       }
   }

   // 添加一个节点到双向链表的最后
   public void add(HeroNode2 heroNode2) {
       // 因为head节点不能动,因此我们需要一个辅助遍历 temp
       HeroNode2 temp = head;
       // 遍历链表,找到最后
       while(true) {
           // 找到链表的最后
           if(temp.next == null) {
               break;
           }
           // 如果没有找到最后,就将temp后移
           temp = temp.next;
       }
       // 当退出while循环时,temp就指向了链表的最后
       // 将最后这个节点的next指向 新的节点
       // 形成一个双向链表
       temp.next = heroNode2;
       heroNode2.pre = temp;
   }

   // 修改一个节点的内容  和单向链表的修改基本一样
   public void update(HeroNode2 newHeroNode) {
       // 判断是否空
       if(head.next == null) {
           System.out.println("链表为空~");
           return;
       }
       // 找到需要修改的节点,根据no编号
       // 定义一个辅助变量
       HeroNode2 temp = head.next;
       boolean flag = false; // 表示是否找到该节点
       while (true) {
           if(temp == null) {
               break;// 已经遍历完链表
           }
           if(temp.no == newHeroNode.no) {
               // 找到
               flag = true;
               break;
           }
           temp = temp.next;
       }
       //根据flag 判断是否找到要修改的节点
       if(flag) {
           temp.name = newHeroNode.name;
           temp.nickname = newHeroNode.nickname;
       } else { // 没有找到
           System.out.printf("没有找到编号 %d 的节点,不能修改\n", newHeroNode.no);
       }


   }

   // 从双向链表中删除一个节点
   // 说明
   // 1、对于双向链表,我们可以直接找到要删除的这个节点
   // 2、找到后,自我删除即可
   public void del(int no) {
       // 判断当前链表是否为空
       if(head.next == null) {
           System.out.println("链表为空,无法删除");
           return;
       }
       HeroNode2 temp = head.next;// 辅助变量(指针)
       boolean flag = false; // 标志是否找到待删除节点的
       while (true) {
           if(temp.next == null) { // 已经走到链表最后节点的next
               break;
           }
           if(temp.next.no == no) {
               // 找到待删除节点的前一个节点temp
               flag = true;
               break;
           }
           temp = temp.next; // temp后移, 遍历
       }
       // 判断flag
       if(flag) { // 找到
           // 可以删除
           temp.pre.next = temp.next;
           // 如果是最后一个节点就不需要执行下面的代码,否则会出现空指针
           if(temp.next != null) {
               temp.next.pre = temp.pre;
           }
       } else {
           System.out.printf("要删除的节点 %d 节点不存在\n", no);
       }
   }

}


// 定义HeroNode2,每个HeroNode2 对象就是一个节点
class HeroNode2 {
   public int no;
   public String name;
   public String nickname;
   public HeroNode2 next;//指向下一个节点  默认为null
   public HeroNode2 pre;//指向前一个节点  默认为null

   // 构造器
   public HeroNode2(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 + '\'' +
               '}';
   }
}

2.3.3 单向环形链表

  • 应用场景:Joseph(约瑟夫、约瑟夫环)问题

  • 构建一个单向的环形链表思路
    1、先创建第一个节点,让first指向该节点,并形成环
    2、后面当我们没创建一个新的节点,就把该节点加入到已有的环形链表中即可

  • 遍历环形链表
    1、先让一个辅助指针(变量)curBoy,指向first节点
    2、然后通过一个while循环遍历该环形链表即可 curBoy.next = first;结束

package com.sss.linkedList;

import org.w3c.dom.ls.LSOutput;

public class Josephu {

    public static void main(String[] args) {
        // 测试一把看看构建环形链表和遍历是否ok
        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
        circleSingleLinkedList.addBoy(5);// 加入5个小孩节点
        circleSingleLinkedList.showBoy();

        // 测试一把 小孩出圈是否正确
        circleSingleLinkedList.countBoy(10,20,125);
    }
}

// 创建一个环形的单向链表
class CircleSingleLinkedList {
    // 创建一个first节点 当前没有编号
    private Boy first = null;
    // 添加小孩节点,构建一个环形的链表
    public void addBoy(int nums) {
        // nums做一个数据校验
        if(nums < 1) {
            System.out.println("nums的值不正确");
            return;
        }
        Boy curBoy = null;// 辅助指针,帮助构建环形链表
        // 使用for来创建我们的环形链表
        for (int i = 1; i <= nums; i++) {
            // 根据编号,创建小孩节点
            Boy boy = new Boy(i);
            // 如果是第一个小孩
            if(i == 1) {
                first = boy;
                first.setNext(first);// 构成环
                curBoy = first;// 让curBoy指向第一个小孩
            } else {
                curBoy.setNext(boy);
                boy.setNext(first);
                curBoy = boy;
            }
        }
    }

    // 遍历环形链表
    public void showBoy() {
        // 判断链表是否为空
        if(first == null) {
            System.out.println("没有任何小孩~~");
            return;
        }
        // 因为first不能动,因此我们仍然使用一个辅助指针完成遍历
        Boy curBoy = first;
        while (true) {
            System.out.printf("小孩的编号 %d \n", curBoy.getNo());
            if(curBoy.getNext() == first) { // 说明遍历完了
                break;
            }
            curBoy = curBoy.getNext();// curBoy后移
        }
    }

    // 根据用户输入,计算出小孩出圈的顺序

    /**
     * @param startNo  表示从第几个小孩开始数数
     * @param countNum 表示数几下
     * @param nums 表示最初小孩的个数
     */
    public void countBoy(int startNo, int countNum, int nums) {
        // 先对数据进行校验
        if (first == null || startNo < 1 || startNo > nums) {
            System.out.println("参数输入有误,请重新输入");
            return;
        }
        // 创建要给辅助指针,帮助小孩完成出圈
        Boy helper = first;
        // 需求创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点
        while (true) {
            if (helper.getNext() == first) {// 说明helper指向最后小孩节点
                break;
            }
            helper = helper.getNext();
            // 小孩报数前,先让first 和 helper移动 k-1次
            for (int i = 0; i < startNo - 1; i++) {
                first = first.getNext();
                helper = helper.getNext();
            }
            // 当小孩报数时,让first 和 helper 同时移动 m-1 次,然后出圈
            for (int i = 0; i < countNum - 1; i++) {
                first = first.getNext();
                helper = helper.getNext();
            }
            // 这时first指向的节点,就是要出圈的小孩的节点
            System.out.printf("小孩 %d  出圈\n", first.getNo());
            // 这时将first指向小孩节点出圈
            first = first.getNext();
            helper.setNext(first);
        }
        System.out.printf("最后留在圈中的小孩编号%d \n", first.getNo());
    }
}

// 创建一个Boy类,表示一个节点
class Boy {
    private int no;// 编号
    private  Boy next;// 指向下一个节点  默认为null

    public Boy(int no) {
        this.no = no;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public Boy getNext() {
        return next;
    }

    public void setNext(Boy next) {
        this.next = next;
    }
}

2.4 栈

栈的一个实际需求-------- 清输入一个表达式 722-5+1-5+3-3 点击计算结果

2.4.1 栈的介绍

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

2.4.2 栈的应用场景

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

2.4.3 实现栈的思路分析

 1、使用数组来模拟栈
 2、定义一个top来表示栈顶,初始化为 -1。
 3、==入栈==的操作,当有数据加入到栈中时,top++; stack[top] = data;
 4、==出栈==的操作,int value = stack[top]; top--; return value;
public class ArrayStackDemo {

 public static void main(String[] args) {
     // 测试一下ArrayStack是否正确
     // 先创建一个ArrayStack对象 ---》表示栈
     ArrayStack stack = new ArrayStack(4);
     String key = "";
     boolean loop = true;//控制是否退出菜单
     Scanner scanner = 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 = scanner.next();
         switch (key) {
             case "show":
                 stack.list();
                 break;
             case "push":
                 System.out.println("请输入一个数");
                 int value = scanner.nextInt();
                 stack.push(value);
                 break;
             case "pop":
                 try {
                     int res = stack.pop();
                     System.out.printf("出栈的数据是 %d\n", res);
                 } catch (Exception e) {
                     System.out.println(e.getMessage());
                 }
                 break;
             case "exit":
                 scanner.close();
                 loop = false;
                 break;
             default:
                 break;
         }
     }
     System.out.println("程序退出~~~");
 }
}

// 定义一个ArrayStack 表示栈
class ArrayStack {
 private int maxSize;// 栈的大小
 private int[] stack;// 数组,数组模拟栈,数据就存放在该数组
 private int top = -1;// top 表示栈顶,初始化为 -1

 // 构造器
 public ArrayStack(int maxSize) {
     this.maxSize = maxSize;
     stack = new int[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 list() {
     if(isEmpty()) {
         System.out.println("栈空,没有数据");
         return;
     }
     // 需要从栈顶开始显示数据
     for(int i = top; i >= 0; i--) {
         System.out.printf("stack[%d] = %d\n", i, stack[i]);
     }
 }
}

2.4.4 使用栈完成表达式计算的思路

  • 通过一个index值(索引),来遍历我们的表达式
  • 如果是数字,就直接入数栈
  • 如果是符号,分两种情况
    • 如果发现当前的符号栈为空,就直接入符号栈
    • 如果符号栈有操作符,就进行比较,如果当前的操作符的优先级小于或等于栈中的操作符,
      就需从数栈中pop出两个数,再从符号栈中pop出一个符号,进行运算,将得到的结果,入数栈,然后将当前的操作符入符号栈;如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈
  • 当表达式扫描完毕,就顺序地从数栈和符号栈中pop出相应的数和符号,并运行
  • 最后在数栈只有一个数据,就是计算结果

2.4.5 栈的前缀、中缀、后缀表达式

中缀表达式转后缀表达式思路:
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中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式

2.5 递归

2.5.1 递归的概念

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

2.5.2 递归的应用场景

迷宫问题(回溯),递归
在这里插入图片描述

2.5.3 递归调用机制

1)打印问题
2)阶乘问题
请添加图片描述
递归调用规则:
1、当程序执行到一个方法时,就会开辟一个独立的空间(栈)
2、每个空间的数据(局部变量),是独立的

2.5.4 递归能解决什么问题

1、各种数学问题:如8皇后问题、汉诺塔、阶乘问题、迷宫问题、球和篮子的问题 (google编程大赛)
2、各种算法中也会用到递归,比如:快排、归并排序、二分查找、分治算法等
3、将用栈解决的问题 —》 递归代码比较简洁

2.5.5 递归需要遵守的重要规则

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

2.5.6 递归—迷宫问题

在这里插入图片描述
代码实现

public class MiGong {
    public static void main(String[] args) {
        // 创建一个二维数组,表示迷宫
        // 地图
        int[][] map = new int[8][7];
        // 使用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[i][6] = 1;
        }
        // 设置挡板
        map[3][1] = 1;
        map[3][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();
        }

        // 使用递归回溯给小球找路
        getWay(map,1,1);

        // 输出新的地图,小球走过,并标识过的递归
        System.out.println("地图的情况");
        for (int i = 0; i < 7; i++) {
            for (int j = 0; j < 8; j++) {
                System.out.print(map[i][j] + " ");
            }
            System.out.println();
        }
    }


    // 使用递归回溯来给小球找路
    // 说明  map表示地图; i、j表示从地图的哪个位置出发(1,1); 如果小球能到map[6][5]位置,则说明通路找到
    // 约定:当map[i][j]为0表示该点没有走过,当为1表示墙;2表示通路可以走;3表示该点已经走过,但是走不通
    // 在迷宫时,需要确定一个策略 下--右--上--左,如果该点走不通,再回溯
    public static boolean getWay(int[][] map, int i, int j) {
        if(map[6][5] == 2) {// 通路已经找到
            return true;
        } else {
            if(map[i][j] == 0) { // 如果当前这个点还没有走过
                map[i][j] = 2;// 假定该点是可以走通的
                if(getWay(map, i, j+1)) { // 向右走
                    return true;
                } else if (getWay(map,i-1, j)) { // 向上走
                    return true;
                } else if (getWay(map, i, j-1)) {
                    return true;
                } else {
                    // 说明该点是走不通,是死路
                    map[i][j] = 3;
                    return false;
                }
            } else { // 如果map[i][j] != 0, 可能是1、2、3
                return false;
            }
        }
    }
}

对迷宫问题的讨论
1、小球得到的路径,和程序员设置的找路策略有关,即:找路的上下左右的顺序有关
2、再得到小球路径时,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化
3、测试回溯现象
4、**思考:**如何求出最短路径?思路》代码实现

public class Quene8 {

    // 定义一个max表示共有多少个皇后
    int max = 8;
    // 定义数组array,保存皇后放置位置的结果,比如 arr = {0,4,7,5,2,6,1,3}
    int[] array = new int[max];
    static int count = 0;
    static int judgeCount = 0;
    public static void main(String[] args) {
        // 测试一把  8皇后是否正确
        Quene8 quene8 = new Quene8();
        quene8.check(0);
        System.out.printf("一共有%d种解法", count);
        System.out.printf("一共判断冲突的次数有%d次", judgeCount);
    }

    // 编写一个方法,放置第n个皇后
    private void check(int n) {
        if(n == max) { // n = 8, 说明前面个皇后已经放好了
            print();
            return;
        }
        // 依次放入皇后并判断是否冲突
        for (int i = 0; i < max; i++) {
            // 先把当前这个皇后 n 放到该行的第 1 列
            array[n] = i;
            // 判断当放置第 n 个皇后到i列时,是否冲突
            if(judge(n)) { // 不冲突
                // 接着放 n+1 个皇后,即开始递归
                check(n+1);
            }
        }
    }

    // 查看当我们放置第n个皇后后,就去检测该皇后是否和前面已经摆放的皇后冲突
    /**
     *
     * @param n 表示第n个皇后
     * @return
     */
    private boolean judge(int n) {
        judgeCount++;
        for (int i = 0; i < n; i++) {
            // 说明:
            // array[i] == array[n] 表示判断 第n个皇后是否和前面 n-1 个皇后在同一列
            // Math.abs(n-i) == Math.abs(array[n]-array[i]) 判断是否在同一斜线上
            if(array[i] == array[n] || Math.abs(n-i) == Math.abs(array[n]-array[i])) {
                return false;
            }
        }
        return true;
    }

    // 写一个方法,可以将皇后摆放的位置输出
    private void print() {
        count++;
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
        System.out.println();
    }
}

2.6 排序算法

2.6.1 排序算法介绍

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

2.6.2 排序的分类

1、内部排序:
指将需要处理的所有数据都加载到**内部存储器(内存)中进行排序。
2、外部排序
数据量过大,无法全部加载到内存中,需要借助
外部存储(文件等)**进行排序
3、常见的排序算法分类:在这里插入图片描述
4、算法时间复杂度
度量一个程序(算法)执行时间的两种方法
(1)事后统计的方法
这种方法可行,但是有两个问题:一是想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,这种方式,要在同一个计算机的相同状态下运行,才能比较哪个算法速度更快。
(2)事前估算的方法
通过分析某个算法的时间复杂度来判断哪个算法更优。

2.6.3 算法的时间复杂度

1、时间频度:一个算法执行的时间与算法中语句执行的次数成正比,哪个算法中语句执行次数多,它花费的时间就多。一个算法中语句执行次数称为语句频度或者时间频度,记为T(n)
2、举例说明—基本案例
比如计算1-100所有数字之和,我们设计两种算法

int total = 0 ;
int end = 100 ;
// 使用for循环计算
for(int i = 1; i < end; i++) {
 total += i;
}
T(n) = n + 1;

// 直接计算
total = (1+end) * end/2
T(n) = 1;

3、时间复杂度
1、一般情况下算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有,某个辅助函数 f(n) ,使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于0的常数,则称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²)

2.6.4 常见的时间复杂度

1、常数阶 O(1)

int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

2、对数阶 O(㏒2n)

int i = 1;
while (i < n) {
i = i * 2;
}

3、线性阶 O(n)

for(int i = 0; i <= n; i++) {
   j = i;
   j++ ;
}

4、线性对数阶O(n㏒2n)

for( m = 1; m < n; m++) {
   i = 1;
   while (i<n) {
   i = i * 2;
   }
}

5、平方阶 O(n²)

for(x=1; x<n; x++) {
	for(i=1; i<n; i++) {
	 j = i;
	 j++;
	}
}

6、立方阶 O(n³)
7、k次方阶 O(n∧k)
参考上面的O(n²)去理解就好了,O(n³) 相当于3层n循环,其他的类似
8、指数阶 O(2∧n)
说明:
1、 常见的算法时间复杂度由小到大依次为: O(1) < O(㏒2n) < O(n) < O(n㏒2n) < O(n²) < O(n³) < O(n∧k) < O(2∧n),随着问题规模n的不断增大,算法的执行效率越低
2、我们应该尽可能避免使用指数阶的算法

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

1、平均时间复杂度是指所有的可能的输入实例均以等概率出现的情况下,该算法运行的时间
2、最坏情况下的时间复杂度称为最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。
3、平均时间复杂度和最坏时间复杂度是否一致,和算法有关。

2.6.6 算法的空间复杂度简介

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

2.7 冒泡排序

2.7.1 基本介绍

冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标比较小的元素开始),依次比较相邻元素,若发现逆序则交换,使得值较大的元素逐渐从前向后移动,就像水下的气泡一样逐渐上冒。
==优化:==因为在排序过程中各个元素不断接近自己的位置,如果一趟比较下来没有进行交换,就说明序列有序。因此要在排序过程中设置一个flag判断元素是否进行过交换,从而较少不必要的比较。(这里说的优化,可以在冒泡排序写好后进行)
在这里插入图片描述
小结:
1、一共进行数组大小 - 1 次 大的循环
2、每一趟排序的次数在逐渐减少
3、如果我们发现在某趟排序中,没有发生过一次交换,可以提前结束冒泡排序,这就是优化。
代码实现

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,10,-2};
//        System.out.println("排序前");
//        System.out.println(Arrays.toString(arr));

        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] =(int)(Math.random() * 8000000); // 生成一个[0, 8000000)数
        }
//        System.out.println(Arrays.toString(arr));

        Date startDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String start = format.format(startDate);
        System.out.println("排序前的时间是:" + start);

        bubbleSort(arr);// 测试冒泡排序
        Date endDate = new Date();
        String end = format.format(endDate);
        System.out.println("排序后的时间是:" + end);


        System.out.println("排序后");
        System.out.println(Arrays.toString(arr));

    }

    public  static void bubbleSort(int[] arr) {
        // 冒泡排序时间复杂度为 o(n²)
        int temp = 0;
        boolean flag = false; // 标识变量,表示是否进行交换
        for (int j = 0; j < arr.length - 1; j++) {
            for (int i = 0; i < arr.length -1 - j; i++) {
                if(arr[i] > arr[i+1]) {
                    flag = true;
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                }
            }

            if(!flag) { // 在一趟排序中,一次交换都没发生过
                break;
            } else {
                flag = false; //重置flag,进行下次判断
            }
        }
    }
}

2.8 选择排序

2.8.1 基本介绍

选择排序也属于内部排序法,是从欲排序的数据中,按执行的规则选出某一元素,再依规定交换位置后达到排序的目的。

2.8.2 排序思想

选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从a[0] - a[n-1]中选取最小值,与a[0]交换,第二次从a[1] - a[n-1]中选取最小值,与a[1]交换,第三次从a[2] - a[n-1]中选取最小值,与a[2]交换…第一次从a[n-2] - a[n-1]中选取最小值,与a[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

代码实现

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class SelectSort {

    public static void main(String[] args) {

        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] =(int)(Math.random() * 8000000); // 生成一个[0, 8000000)数
        }

        Date startDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String start = format.format(startDate);
        System.out.println("排序前的时间是:" + start);

        selectSort(arr);// 测试选择排序
        Date endDate = new Date();
        String end = format.format(endDate);
        System.out.println("排序后的时间是:" + end);

//        System.out.println("排序后");
//        System.out.println(Arrays.toString(arr));
    }

    // 选择排序
    // 选择排序的时间复杂度是 O(n²)  速度比冒泡排序快(在我电脑上基本2s时间)
    public static void selectSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            int min = arr[i];
            for (int j = i; j < arr.length; j++) {
                if(min > arr[j]) {// 说明假定的最小值,并不是最小
                    min = arr[j]; // 重置min
                    minIndex = j; // 重置minIndex
                }
            }
            // 将最小值,放在arr[j], 即交换
            if(minIndex != i) {
                arr[minIndex] =arr[i];
                arr[i] = min;
            }
        }
    }
}

2.9 插入排序

2.9.1 插入排序的基本介绍

插入排序属于内部式排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。

2.9.2 插入排序法思想

把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。直接插入排序示例如下图:
在这里插入图片描述

package com.sss.sort;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class InsertSort {

    public static void main(String[] args) {
        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] =(int)(Math.random() * 8000000); // 生成一个[0, 8000000)数
        }

        Date startDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String start = format.format(startDate);
        System.out.println("排序前的时间是:" + start);

        insertSort(arr);// 测试选择排序
        Date endDate = new Date();
        String end = format.format(endDate);
        System.out.println("排序后的时间是:" + end);
    }

    // 插入排序
    public static void insertSort(int[] arr) {
        int insertValue = 0;;
        int insertIndex = 0;
        for (int i = 0; i < arr.length; i++) {
            // 定义待插入的数
            insertValue = arr[i];;
            insertIndex = i - 1; // 即arr[i]这个待插入数的下标的前一个位置

            while(insertIndex >= 0 && insertValue < arr[insertIndex]) {
                arr[insertIndex + 1] = arr[insertIndex];
                insertIndex--;
            }
            if(insertIndex + 1 != i) {
                arr[insertIndex + 1] = insertValue;
            }
        }
    }
}

3.0 希尔排序

3.0.1 简单插入排序存在的问题

数组 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}
结论:当需要插入的数是较小的数时,后移的次数明天增多,对效率有影响

3.0.2 希尔排序法基本介绍

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

3.0.3 希尔排序法基本思想

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序。随着增量逐渐减少,每组包含的关键词越来越多,当增量减为1时,整个文件恰被分成一组,算法便终止。
在这里插入图片描述

在这里插入图片描述

3.0.4 希尔排序法应用实例:

有一群小牛,考试成绩分别是{8,9,1,7,2,3,5,4,6,0} 请从小到大排序,请分别使用:
1)希尔排序时,对有序序列在插入时采用交换法,并测试排序速度
2)希尔排序时,对有序序列在插入时采用移动法,并测试排序速度

代码实现

import java.text.SimpleDateFormat;
import java.util.Date;

public class ShellSort {
    public static void main(String[] args) {
        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] =(int)(Math.random() * 8000000); // 生成一个[0, 8000000)数
        }

        Date startDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String start = format.format(startDate);
        System.out.println("排序前的时间是:" + start);

        shellSort2(arr);// 交换法
        shellSort2(arr);// 移位法
        Date endDate = new Date();
        String end = format.format(endDate);
        System.out.println("排序后的时间是:" + end);
    }

    // 希尔排序时,对有序序列在插入时采用交换法
    // 思路(算法) ==》 代码
    public static void shellSort(int[] arr) {
        int temp = 0;
        int count = 0;
        // 根据前面的逐步分析,使用循环处理
        for (int gap = arr.length/2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                // 遍历各组内所有的元素(共gap组,每组有2个元素),步长gap
                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;
                    }
                }
            }
        }
    }

    // 对交换式的希尔排序进行优化 -》 移位法
    public static void shellSort2(int[] arr) {
        // 增量gap, 并逐步地缩小增量
        for(int gap = arr.length / 2; gap > 0; gap /= 2) {
            // 从第 gap 个元素,逐个对其所在的组进行直接插入排序
            for(int i = gap; i < arr.length; i++) {
                int j = i;
                int temp = arr[j];
                if (arr[j] < arr[j - gap]) {
                    while (j - gap >= 0 && temp < arr[j - gap]) {
                        // 移动
                        arr[j] = arr[j - gap];
                        j -= gap;
                    }
                    // 当退出while后,就给temp找到插入的位置
                    arr[j] = temp;
                }
            }
        }
    }
}

3.1 快速排序

3.1.1 基本介绍

快速排序(quickSort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据要小,然后再按照此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据编程有序序列。

3.1.2 示意图

在这里插入图片描述

3.3.3 快速排序法应用实例

要求: 对 [-9,78,0,23,-567,70] 进行从小到大的排序,要求使用快速排序法【测试80w 和 800w】

说明[验证分析]:
1)如果取消左右递归,结果是 -9 -567 0 23 78 70
2)如果取消右递归,结果是 -567 -9 0 23 78 70
3)如果取消左右递归,结果是 -9 -567 0 23 70 78

3.3.4 代码实现

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class QuickSort {
    public static void main(String[] args) {
//        int[] arr = {-9,-554,2223,555,0,33,55,1,-700};
//        quickSort(arr, 0, arr.length - 1);
//        System.out.println("arr =" + Arrays.toString(arr));

        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] =(int)(Math.random() * 8000000); // 生成一个[0, 8000000)数
        }

        Date startDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String start = format.format(startDate);
        System.out.println("排序前的时间是:" + start);

        quickSort(arr, 0, arr.length - 1);// 测试选择排序
        Date endDate = new Date();
        String end = format.format(endDate);
        System.out.println("排序后的时间是:" + end);
    }

    public static void quickSort(int[] arr, int left, int right) {
        int l = left;// 左下标
        int r = right;// 右下标
        // pivot 中轴值
        int pivot = arr[(left + right) / 2];
        int temp = 0; // 临时变量 作为交换时使用
        //while 循环的目的是让比pivot小的值放到左边
        //比pivot大的值放到右边
        while (l < r) {
            // 在pivot左边一直找,找到大于等于pivot的值,才退出
            while (arr[l] < pivot) {
                l += 1;
            }
            // 在pivot右边一直找,找到小于等于pivot的值,才退出
            while (arr[r] > pivot) {
                r -= 1;
            }
            // 如果 l >= r 说明pivo左右的值,已经按照左边全部是小于等于 pivot,右边全部是大于等于pivot值
            if( l >= r) {
                break;
            }

            // 交换
            temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;
            // 如果交换完后,发现这个arr[r] == pivot值 l++ 后移
            if(arr[l] == pivot) {
                l += 1;
            }
        }

        // 如果 l == r, 必须l++ r--, 否则会出现栈溢出
        if(l == r) {
            l++;
            r--;
        }
        // 向左递归
        if(left < r) {
            quickSort(arr, left, r);
        }
        // 向右递归
        if(right > l) {
            quickSort(arr, left, r);
        }
    }
}

3.4 归并排序

3.4.1 基本介绍

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

3.4.2 图示

在这里插入图片描述
代码实现

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,7,9,2,1,5,45};
//        int[] temp =  new int[arr.length]; // 归并需要一个额外空间
//        mergeSort(arr,0,arr.length - 1, temp);
//        System.out.println("归并排序后:" + Arrays.toString(arr));
        int[] arr = new int[80000];
        int[] temp =  new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] =(int)(Math.random() * 8000000); // 生成一个[0, 8000000)数
        }

        Date startDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String start = format.format(startDate);
        System.out.println("排序前的时间是:" + start);
        mergeSort(arr, 0, arr.length - 1, temp);// 测试选择排序
        Date endDate = new Date();
        String end = format.format(endDate);
        System.out.println("排序后的时间是:" + end);
    }

    // 分+合方法
    public static void mergeSort(int[] arr, int left, int right, int[] temp) {
        if(left < right) {
            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; // 初始化i  左边有序序列的初始索引
        int j = mid + 1; // 初始化j  右边有序序列的初始索引
        int t = 0; // 指向temp数组的当前索引

        // (一) 先把左右两边(有序)的数据按照规则填充到temp,直到左右两边的有序序列,有一边处理完毕为止
        while (i <= mid && j <= right) {// 继续
            // 如果左边有序序列的当前元素  小于等于右边有序序列的当前元素,即将左边的当前元素,填充到temp 然后t++ i++
            if(arr[i] <= arr[j]) {
                temp[t] = arr[i];
                t++;
                i++;
            } else {// 反之 将右边有序序列的当前元素  填充到temp数组
                temp[t] = arr[j];
                t++;
                j++;
            }
        }

        // (二) 把有剩余数据的一边的数据依次全部填充到temp
        while (i <= mid) {// 左边的有序序列还有剩余的元素  就全部填充到temp
            temp[t] = arr[i];
            t++;
            i++;
        }
        while (j <= right) { // 右边有序序列还有剩余的元素,就全部填充到temp
            temp[t] = arr[j];
            t++;
            j++;
        }

        // (三) 将temp数组的全部元素拷贝到arr
        // 注意,并不是每次都拷贝所有
        t = 0;
        int tempLeft = left;
        // 第一次合并tempLeft = 0, right = 1  最后一次tempLeft = 0, right = 7
//        System.out.println("tempLeft = " + tempLeft + "  " + "right = " + right);
        while (tempLeft <= right) {
            arr[tempLeft] = temp[t];
            t++;
            tempLeft++;
        }

    }
}

3.5 基数排序

3.5.1 基本介绍

1)基数排序属于“分配式排序”,又称“桶子法”,它是通过键值各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
2)基数排序法是属于稳定性排序,基数排序法是效率高的稳定性排序法
3)基数排序是桶排序的扩展
4)基数排序是1887年赫尔曼 何乐礼发明的。它是这样实现的:将整数按照位数切割成不同的数字,然后按照每个位数分别比较。

3.5.2 基本思想

1)将所有待比较数值统一为同样的位数长度,位数较短的数前面补零。然后从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成后,数列就变成了一个有序序列。

代码实现

package com.sss.sort;

import java.text.SimpleDateFormat;
import java.util.Date;

public class RadixSort {
    public static void main(String[] args) {

        // 800000000 * 11 * 4 / 1024 / 1024 / 1024 = 3.3G
        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] =(int)(Math.random() * 8000000); // 生成一个[0, 8000000)数
        }

        Date startDate = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String start = format.format(startDate);
        System.out.println("排序前的时间是:" + start);

        radixSort(arr);// 测试选择排序
        Date endDate = new Date();
        String end = format.format(endDate);
        System.out.println("排序后的时间是:" + end);
    }

    // 基数排序方法
    public static void radixSort(int[] arr) {
        // 根据前面的推导过程  我们可以得到最终的基数排序代码

        // 1、得到数组中最大的数的位数
        int max = arr[0];// 假设第一个数就是最大数
        for (int i = 0; i < arr.length; i++) {
            if(arr[i] > max) {
                max = arr[i];
            }
        }
        // 得到最大数是几位数
        int maxLength = (max + "").length();

        // 定义一个二维数组,表示10个桶,每一个桶都是一个一维数组
        /*
         说明:
         1、二维数组包含10个一维数组
         2、为了防止在放入数的时候,数据溢出,则每一个二维数组(桶),大小定为 arr.length
         3、明确:基数排序是使用空间换时间的经典算法
         */
        int[][] bucket = new int[10][arr.length];

        //为了记录每个桶中实际放了多少个数据,我们定义一个一维数组才记录每个桶中放入数据的个数
        // 可以理解  比如 bucketElementCount[0],记录的就是 bucket[0]桶放入的数据个数
        int[] bucketElementCount = 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;
                // 放到对应的桶中
                bucket[digitOfElement][bucketElementCount[digitOfElement]] = arr[i];
                bucketElementCount[digitOfElement]++;
            }
            // 按照这个桶的顺序(一维数组的下标一次取出数据放入到原来数组)
            int index = 0;
            // 遍历每一个桶  并将桶中数据放入原数组
            for (int k = 0; k < bucketElementCount.length; k++) {
                // 如果桶中有数据  我们就放入原数组
                if(bucketElementCount[k] != 0) {
                    // 循环该桶即第k个桶(即第k个一维数组),放入
                    for (int l = 0; l < bucketElementCount[k]; l++) {
                        // 取出元素 放入到arr
                        arr[index++] = bucket[k][l];
                    }
                }
                // 第 i+1 轮处理后,需要将每个bucketElementCount[k] = 0 !!!
                bucketElementCount[k] = 0;
            }
        }
    }
}

说明

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

4. 查找算法

在java中,我们常用的查找有4种:
1)顺序(线性)查找
2)二分查找/折半查找
3)插值查找
4)斐波那契查找

4.1 线性查找

代码实现

package com.sss.search;

public class SeqSearch {
    public static void main(String[] args) {
        int[] arr = {13,4,1,6,8,4,33};
        int i = seqSearch(arr, 33);
        if(i == -1) {
            System.out.println("没有找到");
        } else {
            System.out.println("找到,下标为:" + i);
        }
    }

    public static int seqSearch(int[] arr, int value) {
        for (int i = 0; i < arr.length; i++) {
            if(arr[i] == value) {
                return i;
            }
        }
        return -1;
    }
}

4.2 二分查找

4.2.1 二分查找

请对一个有序数组进行二分查找{1,8,10,89,1000,1234},输入一个数,看看该数组是否存在这个数,并求出下标,如果没有就提示“没有这个数”。

4.2.2 思路分析

1、首先确认该数组的中间下标 mid = (left + right) / 2
2、然后让需要查找到的数finalValue和arr[mid]比较
2.1 finalValue > arr[mid] ,说明要查找的数在mid右边,因此需要递归向右边查找
2.2 finalValue < arr[mid],说明要查找的数在mid左边,因此需要递归向左边查找
2.3 finalValue = arr[mid], 说明找到了 返回即可
3、什么时候结束递归
3.1 找到就结束递归
3.2 递归完整个数组,仍然没有找到finalValue,也需要结束递归,当left > right 就退出

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class binarySearch {
    public static void main(String[] args) {
        int[] arr = {11,333,44,222,543,4534,111,5453,44,21};
        Arrays.sort(arr);
        List<Integer> resIndexList = binarySearch2(arr, 0, arr.length - 1, 44);
        System.out.println("resIndexList:" + resIndexList);
    }

    // 二分查找法
    public static int binarySearch(int[] arr, int left, int right, int finalValue) {
        // 当left > right 时,说明递归整个数组,但是没有找到
        if(left > right) {
            return -1;
        }
        int mid = (left + right) / 2;
        if(finalValue > arr[mid]) { // 向右递归
           return binarySearch(arr, mid+1, right, finalValue);
        } else if (finalValue < arr[mid]) { // 向左递归
           return binarySearch(arr, left, mid - 1, finalValue);
        } else {
            return mid;
        }
    }

    public static List<Integer> binarySearch2(int[] arr, int left, int right, int finalValue) {
        // 当left > right 时,说明递归整个数组,但是没有找到
        if(left > right) {
            return null;
        }
        int mid = (left + right) / 2;
        if(finalValue > arr[mid]) { // 向右递归
            return binarySearch2(arr, mid+1, right, finalValue);
        } else if (finalValue < arr[mid]) { // 向左递归
            return binarySearch2(arr, left, mid - 1, finalValue);
        } else {
            /**
             * 思路分析:
             * 1、在查找到mid索引值,不要马上返回
             * 2、向mid索引值的左边扫描,将所有满足1000 的元素的下标,加入到集合ArrayList
             * 3、向mid索引值的右边扫描,将所有满足1000 的元素的下标,加入到集合ArrayList
             * 4、将ArrayList 返回
             */
            List<Integer> resIndexList = new ArrayList<>();
            resIndexList.add(mid);
            // 向mid索引值的左边扫描
            int temp = mid - 1;
            while (true) {
                if(temp < 0 || arr[temp] != finalValue) {
                    break;
                }
                // 否则 就将temp放入到 resIndexList
                resIndexList.add(temp);
                temp--;
            }
            //向mid索引值的右边扫描
            temp = mid+1;
            while (true) {
                if(temp > arr.length -1 || arr[temp] != finalValue) {
                    break;
                }
                resIndexList.add(temp);
                temp++;
            }
            return resIndexList;
        }
    }
}

4.3 插值查找算法

4.3.1 基本介绍

1、原理介绍:插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。
2、将折半查找中的求mid索引的公式,low表示左边索引left,high表示右边索引right,key就是我们前面讲的findValue
3、int mid = low + (high - low) * (key - arr[low]) / (arr[hign] - arr[low]); 插值索引
对应前面的公式:
int mid = left + (right - left) * (findValue - arr[left]) / (arr[right] - arr[left])
在这里插入图片描述

4、举例说明插值查找算法1-100数组

代码实现

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 index = insertValueSearch(arr, 0, arr.length - 1, 1);
        System.out.println("index=" + index);
    }

    // 编写插值算法  也要求数组是有序的
    public static int insertValueSearch(int[] arr, int left, int right, int finalValue) {
        // 注意 arr[0] > finalValue || arr[arr.length-1] < finalValue 是必须的,否则会出现mid越界
        if(left > right || arr[0] > finalValue || arr[arr.length-1] < finalValue) {
            return -1;
        }

        // 求出mid  自适应
        int mid = left + (right - left) * (finalValue - arr[left]) / (arr[right] - arr[left]);
        int midVal = arr[mid];
        if(finalValue > midVal) {// 说明向右递归
            return insertValueSearch(arr, mid+1, right, finalValue);
        } else if (finalValue < arr[mid]) {// 说明向左递归
            return insertValueSearch(arr, left, mid-1, finalValue);
        } else {
            return mid;
        }
    }
}

4.3.2 插值查找算法的注意事项

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

4.4 哈希表

4.4.1 基本介绍

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

4.4.2 内存布局图

在这里插入图片描述

package com.sss.hashTable;

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("exit: 退出系统");

            key = scanner.next();
            switch (key) {
                case "add":
                    System.out.println("输入id");
                    int id = scanner.nextInt();
                    System.out.println("输入name");
                    String name = scanner.next();
                    // 创建雇员
                    Emp emp = new Emp(id, name);
                    hashTab.add(emp);
                    break;
                case "list":
                    hashTab.list();
                    break;
                case "exit":
                    scanner.close();
                    System.exit(0);
                default:
                    break;
            }
        }
    }
}

// 创建HashTab  管理多条链表
class HashTab {
    private EmpLinkedList[] empLinkedListArray;
    private int size; // 表示有多少条链表

    // 构造器
    public HashTab(int size) {
        this.size = size;
        empLinkedListArray = new EmpLinkedList[size];
        // 这时要分别初始化每一个链表
        for (int i = 0; i < size; i++) {
            empLinkedListArray[i] = new EmpLinkedList();
        }
    }

    // 添加雇员
    public void add(Emp emp) {
        // 根据员工的id,得到该员工应当添加到哪条链表
        int empLinkedListNO = hashFun(emp.id);
        // 将emp添加到对用链表中
        empLinkedListArray[empLinkedListNO].add(emp);
    }

    // 遍历所有的链表  遍历hashTab
    public void list() {
        for (int i = 0; i < size; i++) {
            empLinkedListArray[i].list(i+1);
        }
    }

    // 编写散列函数,使用一个简单的取模法
    public int hashFun (int id) {
        return id % size;
    }
}

// 表示一个雇员
class Emp {
    public int id;
    public String name;
    public Emp next; // next 默认为空

    public Emp(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

// 创建EmpLinkedList 表示链表
class EmpLinkedList {
    // 头指针,执行第一个Emp, 因此我们这个链表的head 是直接指向第一个Emp
    private Emp head;// 默认 null
    // 添加雇员到链表
    // 说明
    // 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;
        }
        // 退出时直接将Emp 加入链表
        curEmp.next = emp;
    }

    // 遍历链表的雇员信息
    public void list(int no) {
        if(head == null) {
            System.out.println("第"+ no +"条链表为空!");
            return;
        }
        System.out.println("第"+ no + "条链表信息为");
        Emp curEmp = head;// 辅助指针
        while (true) {
            System.out.printf("=> id = %d name = %s\t", curEmp.id, curEmp.name);
            if(curEmp.next == null) {// 说明curEmp已经是最后结点
                break;
            }
            curEmp = curEmp.next;// 后移  遍历
        }
        System.out.println();
    }
}

4.5 二叉树

4.5.1 为什么需要树这种数据结构

  • 数组存储方式的分析

    • 优点:通过下标方式访问元素,速度快。对于有序数组,还可以使用二分查找来提高检索速度。
    • 缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率低
      在这里插入图片描述
  • 链式存储方式的分析

    • 优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,删除效率也很高)
    • 缺点:在进行检索时,效率仍然很低(比如检索某个值,需要从头检点开始遍历)
    • 在这里插入图片描述
  • 树存储方式的分析

    • 能提高数据存储,读取的效率,比如使用二叉排序树,即可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
    • 在这里插入图片描述在这里插入图片描述

4.5.2 树的示意图和常用术语

在这里插入图片描述

  • 节点、根节点、父节点、子节点、叶子节点(没有子节点的节点)、节点的权(节点值)、路径(从root节点找到该节点的路径)、层、子树、树的高度(最大层数)、森林(多颗子树构成森林)

4.5.3 二叉树的概念

1)树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树
2)二叉树的子节点分别为左节点和有节点。
在这里插入图片描述

3) 如果该二叉树的所有叶子节点都在最后一层,并且节点总数= 2ⁿ - 1,n为层数,则我们称为满二叉树
在这里插入图片描述
4)如果该二叉树的所有叶子节点都在最后一层或倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
在这里插入图片描述

2.5.4 二叉树遍历的说明

使用前序、中序和后序对下面的二叉树进行遍历:
1)前序遍历:先输出父节点,再遍历左子树和右子树
2)中序遍历:先遍历左子树,再输出父节点,再遍历右子树
3)后序遍历:先遍历左子树,再遍历右子树,最后输出父节点
小结:看输出父节点的顺序

public class BinaryTreeDemo {
    public static void main(String[] args) {
        // 先创建一个二叉树
        BinaryTree binaryTree = new BinaryTree();
        // 创建需要的节点
        HeroNode root = new HeroNode(1,  "宋江");
        HeroNode heroNode2 = new HeroNode(2,  "吴用");
        HeroNode heroNode3 = new HeroNode(3,  "卢俊义");
        HeroNode heroNode4 = new HeroNode(4,  "林冲");
        HeroNode heroNode5 = new HeroNode(5,  "关胜");

        // 我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
        root.setLeft(heroNode2);
        root.setRight(heroNode3);
        heroNode3.setRight(heroNode4);
        heroNode3.setLeft(heroNode5);
        binaryTree.setRoot(root);

        // 测试前序遍历
        System.out.println("前序遍历:");
        binaryTree.preOrder();

        // 测试中序遍历
        System.out.println("中序遍历:");
        binaryTree.infixOrder();

        // 测试后序遍历
        System.out.println("后序遍历:");
        binaryTree.postOrder();

    }
}

// 定义BinaryTree 二叉树
class BinaryTree {
    private HeroNode root;

    public void setRoot(HeroNode root) {
        this.root = root;
    }

    // 前序遍历
    public void preOrder() {
        if(this.root != null) {
            this.root.preOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 中序遍历
    public void infixOrder() {
        if(this.root != null) {
            this.root.infixOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 后序遍历
    public void postOrder() {
        if(this.root != null) {
            this.root.postOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }
}


class HeroNode {
    private int no;
    private String name;
    private HeroNode left;// 默认为空
    private HeroNode right;// 默认为空

    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    // 编写前序遍历的方法
    public void preOrder() {
        System.out.println(this);// 先输出父节点
        // 递归向左子树前序遍历
        if(this.left != null) {
            this.left.preOrder();
        }
        // 递归向右子树前序遍历
        if(this.right != null) {
            this.right.preOrder();
        }
    }
    // 中序遍历
    public void infixOrder() {
        // 递归向左子树中序遍历
        if(this.left != null) {
            this.left.infixOrder();
        }
        // 输出父节点
        System.out.println(this);
        // 递归向右子树中序遍历
        if(this.right != null) {
            this.right.infixOrder();
        }
    }
    // 后序遍历
    public void postOrder() {
        // 递归向左子树后序遍历
        if(this.left != null) {
            this.left.postOrder();
        }
        // 递归向右子树后序遍历
        if(this.right != null) {
            this.right.postOrder();
        }
        // 输出父节点
        System.out.println(this);
    }
}

2.5.5 使用前序、中序、后序的方式来查询指定的节点

在这里插入图片描述

public class BinaryTreeDemo {
    public static void main(String[] args) {
        // 先创建一个二叉树
        BinaryTree binaryTree = new BinaryTree();
        // 创建需要的节点
        HeroNode root = new HeroNode(1,  "宋江");
        HeroNode heroNode2 = new HeroNode(2,  "吴用");
        HeroNode heroNode3 = new HeroNode(3,  "卢俊义");
        HeroNode heroNode4 = new HeroNode(4,  "林冲");
        HeroNode heroNode5 = new HeroNode(5,  "关胜");

        // 我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
        root.setLeft(heroNode2);
        root.setRight(heroNode3);
        heroNode3.setRight(heroNode4);
        heroNode3.setLeft(heroNode5);
        binaryTree.setRoot(root);

        // 测试前序遍历
        System.out.println("前序遍历:");
        binaryTree.preOrder();

        // 测试中序遍历
        System.out.println("中序遍历:");
        binaryTree.infixOrder();

        // 测试后序遍历
        System.out.println("后序遍历:");
        binaryTree.postOrder();

        // 前序遍历
        System.out.println("前序遍历方式··");
        HeroNode node = binaryTree.preOrderSearch(5);
        if(node != null) {
            System.out.printf("找到了,信息为 no=%d name=%s", node.getNo(), node.getName());
        } else {
            System.out.printf("没有找到 no = %d 的英雄", 5);
        }

    }
}

// 定义BinaryTree 二叉树
class BinaryTree {
    private HeroNode root;

    public void setRoot(HeroNode root) {
        this.root = root;
    }

    // 前序遍历
    public void preOrder() {
        if(this.root != null) {
            this.root.preOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 中序遍历
    public void infixOrder() {
        if(this.root != null) {
            this.root.infixOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 后序遍历
    public void postOrder() {
        if(this.root != null) {
            this.root.postOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 前序遍历
    public HeroNode preOrderSearch(int no) {
        if(root != null) {
            return root.preOrdersearch(no);
        } else {
            return null;
        }
    }

    // 中序遍历
    public HeroNode infixOrderSearch(int no) {
        if(root != null) {
            return root.infixOrderSearch(no);
        } else {
            return null;
        }
    }

    // 后序遍历
    public HeroNode postOrderSearch(int no) {
        if(root != null) {
            return root.postOrderSearch(no);
        } else {
            return null;
        }
    }
}


class HeroNode {
    private int no;
    private String name;
    private HeroNode left;// 默认为空
    private HeroNode right;// 默认为空

    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    // 编写前序遍历的方法
    public void preOrder() {
        System.out.println(this);// 先输出父节点
        // 递归向左子树前序遍历
        if(this.left != null) {
            this.left.preOrder();
        }
        // 递归向右子树前序遍历
        if(this.right != null) {
            this.right.preOrder();
        }
    }
    // 中序遍历
    public void infixOrder() {
        // 递归向左子树中序遍历
        if(this.left != null) {
            this.left.infixOrder();
        }
        // 输出父节点
        System.out.println(this);
        // 递归向右子树中序遍历
        if(this.right != null) {
            this.right.infixOrder();
        }
    }
    // 后序遍历
    public void postOrder() {
        // 递归向左子树后序遍历
        if(this.left != null) {
            this.left.postOrder();
        }
        // 递归向右子树后序遍历
        if(this.right != null) {
            this.right.postOrder();
        }
        // 输出父节点
        System.out.println(this);
    }

    // 前序遍历查找
    public HeroNode preOrdersearch(int no) {
        // 比较当前节点是不是
        if(this.no == no) {
            return this;
        }
        // 1、判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
        // 2、如果左递归前序查找,找到节点,则返回
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.preOrdersearch(no);
        }
        if(resNode != null) { // 说明我们左子树找到
            return resNode;
        }
        // 1、左递归前序查找,找到节点,则返回,否继续判断
        // 2、当前节点的右子节点是否为空,如果不空,则继续向右递归前序查找
        if(this.right != null) {
            resNode = this.right.preOrdersearch(no);
        }
        return resNode;
    }

    // 中序遍历查找
    public HeroNode infixOrderSearch(int no) {
        // 判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.infixOrderSearch(no);
        }
        if(resNode != null) {
            return resNode;
        }
        // 如果找到,则返回,如果没有找到,就和当前节点比较,如果是则返回当前节点
        if(this.no == no) {
            return this;
        }
        // 否则继续进行右递归的中序查找
        if(this.right != null) {
            resNode = this.right.infixOrderSearch(no);
        }
        return resNode;
    }

    // 后序遍历查找
    public HeroNode postOrderSearch(int no) {
        // 判断当前节点的左子节点是否为空,如果不为空,则递归后序查找
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.postOrderSearch(no);
        }
        if(resNode != null) { // 说明在左子树找到
            return resNode;
        }
        // 如果左子树没有找到,则向右子树递归进行后序遍历查找
        if(this.right != null) {
            resNode = this.right.postOrderSearch(no);
        }
        if(resNode != null) {
            return resNode;
        }
        // 如果左右子树都没有找到,就比较当前节点是不是
        if(this.no == no) {
            return this;
        }
        return resNode;
    }
}

2.5.6 二叉树删除节点思路图解

完成删除节点的操作:
规定:
1)如果删除节点是叶子节点,则删除该节点
2)如果删除的节点是非叶子节点,则删除该子树

思路
1、因为我们的二叉树是单向的,所以我们是判断当前节点的子节点是否需要删除节点,而不能去判断当前这个节点是不是需要删除节点
2、如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,就将this.left = null;并且就返回(结束递归删除)
3、如果当前节点的右子节点不为空,并且右子节点就是要删除节点,就将this.right = null; 并且就返回(结束递归删除)
4、如果第2和第3步没有删除节点,那么我们就需要向左子树进行递归删除
5、如果第4步也没有删除节点,则应当向右子树进行递归删除
6、考虑如果树是空树root,如果只有一个root节点,则等价将二叉树置空

package com.sss.tree;

public class BinaryTreeDemo {
    public static void main(String[] args) {
        // 先创建一个二叉树
        BinaryTree binaryTree = new BinaryTree();
        // 创建需要的节点
        HeroNode root = new HeroNode(1,  "宋江");
        HeroNode heroNode2 = new HeroNode(2,  "吴用");
        HeroNode heroNode3 = new HeroNode(3,  "卢俊义");
        HeroNode heroNode4 = new HeroNode(4,  "林冲");
        HeroNode heroNode5 = new HeroNode(5,  "关胜");

        // 我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
        root.setLeft(heroNode2);
        root.setRight(heroNode3);
        heroNode3.setRight(heroNode4);
        heroNode3.setLeft(heroNode5);
        binaryTree.setRoot(root);

        // 测试前序遍历
        System.out.println("前序遍历:");
        binaryTree.preOrder();

       // 测试删除节点
        System.out.println("删除前遍历");
        binaryTree.delNode(5);
        System.out.println("删除后遍历");

    }
}

// 定义BinaryTree 二叉树
class BinaryTree {
    private HeroNode root;

    public void setRoot(HeroNode root) {
        this.root = root;
    }

    // 删除节点
    public void delNode(int no) {
        if(root != null) {
            // 如果只有一个root节点,这里立即判断root是不是就是要删除的节点
            if(root.getNo() == no) {
                root = null;
            } else {
                // 递归删除
                root.delNode(no);
            }
        } else {
            System.out.printf("空树,不能删除");
        }
    }

    // 前序遍历
    public void preOrder() {
        if(this.root != null) {
            this.root.preOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }
}


class HeroNode {
    private int no;
    private String name;
    private HeroNode left;// 默认为空
    private HeroNode right;// 默认为空

    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    // 递归删除节点
    public void delNode(int no) {
        if(this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        if(this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        if(this.left != null) {
            this.left.delNode(no);
        }
        if(this.right != null) {
            this.right.delNode(no);
        }
    }

    // 编写前序遍历的方法
    public void preOrder() {
        System.out.println(this);// 先输出父节点
        // 递归向左子树前序遍历
        if(this.left != null) {
            this.left.preOrder();
        }
        // 递归向右子树前序遍历
        if(this.right != null) {
            this.right.preOrder();
        }
    }
}

2.5.6 顺序存储二叉树思路图解

2.5.6.1 概念

基本说明:
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转化为树,树也可以转化为数组。
在这里插入图片描述
要求
1)上图的二叉树的节点,要求以数组的方式存放 arr: [1,2,3,4,5,6]
2)要求在遍历数组arr时,仍然可以以前序遍历、中序遍历、后序遍历的方式完成节点的遍历

顺序存储二叉树的特点
1)顺序存储二叉树通常只考虑完全二叉树
2)第n个元素的左子节点为 2* n +1
3) 第n个元素的右子节点为2 * n + 2
4) 第n个元素的父节点为(n-1)/ 2
5)n:表示二叉树中的第几个元素(按0开始编号)

public class ArrBinaryTreeDemo {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6};
        // 创建一个ArrBinaryTree
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
        arrBinaryTree.preOrder();
    }
}

// 编写一个ArrBinaryTree,实现顺序存储二叉树遍历
class ArrBinaryTree {
    private int[]  arr;//存储数据节点的数组

    public ArrBinaryTree(int[] arr) {
        this.arr = arr;
    }

    // 重载方法
    public void preOrder() {
        this.preOrder(0);
    }

    // 编写一个方法,完成顺序存储二叉树的前序遍历
    public void preOrder(int index) {
        // 如果数组为空,或者arr.length = 0
        if(arr == null || arr.length == 0) {
            System.out.println("数组为空,不能按照二叉树的前序遍历");
        }
        // 输出当前这个元素
        System.out.println(arr[index]);
        // 向左递归遍历
        if((index * 2 + 1) < arr.length) {
            preOrder(index * 2 + 1);
        }
        // 向右递归遍历
        if((index * 2 + 2) < arr.length) {
            preOrder(index * 2 + 2);
        }
    }
}

2.5.7 线索二叉树

2.5.7.1 基本介绍

1)n个结点的二叉链表中含有n+1 【公式2n - (n-1) = n + 1】个空指针域。利用二叉链表中的空指针域,存放指向该节点在某种遍历次序下的前驱和后继节点的指针(这种附加的指针称为线索)
2)这种加上了线索的二叉链表称为线索二叉链表,响应的二叉树称为线索二叉树。根据线索性质的不同,线索二叉树可分为前序线索二叉树中序线索二叉树后序线索二叉树
3)一个节点的前一个节点,称为前驱节点
4)一个节点的后一个节点,称为后继节点

当线索化二叉树后,Node节点的属性left和right,有如下情况:
1)left 指向的是左子树,也可能指向的是前驱节点
2)right指向的是右子树,也可能指向的是后驱节点
在这里插入图片描述

2.5.7.2 线索化二叉树代码实现
package com.sss.tree.ThreadedBinaryTree;

public class ThreadedBinaryTreeDemo {
    public static void main(String[] args) {
        // 测试一把中序线索二叉树
        HeroNode root = new HeroNode(1, "tom");
        HeroNode node2 = new HeroNode(3, "jack");
        HeroNode node3 = new HeroNode(6, "smith");
        HeroNode node4 = new HeroNode(8, "mary");
        HeroNode node5 = new HeroNode(10, "king");
        HeroNode node6 = new HeroNode(14, "dim");

        root.setLeft(node2);
        root.setRight(node3);
        node2.setLeft(node4);
        node2.setRight(node5);
        node3.setLeft(node6);

        // 测试中序线索化
        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
        threadedBinaryTree.setRoot(root);
        threadedBinaryTree.threadedNodes();

        // 测试
        HeroNode left = node5.getLeft();
        HeroNode right = node5.getRight();
        System.out.println("10号节点的前驱节点是" + left);
        System.out.println("10号节点的后继节点是" + right);

    }
}


// 定义ThreadBinaryTree 实现了线索化功能的二叉树
class ThreadedBinaryTree {
    private HeroNode root;

    // 为了实现线索化,需要创建给指向相当节点的前驱节点的指针
    // 在递归进行线索化时,pre总是保留前一个节点
    private HeroNode pre;

    public void setRoot(HeroNode root) {
        this.root = root;
    }

    // 重载一把
    public void threadedNodes() {
        this.threadedNodes(root);
    }

    // 编写对二叉树进行中序线索化的方法

    /**
     *
     * @param node 就是当前需要线索化的节点
     */
    public void threadedNodes(HeroNode node) {
        if(node == null) {
            return;
        }
        // 1、先线索化左子树
        threadedNodes(node.getLeft());
        // 2、线索化当前节点
        if(node.getLeft() == null) {
            // 让当前节点的左指针指向前一个节点
            node.setLeft(pre);
            // 修改当前节点的左指针类型,指向前驱节点
            node.setLeftType(1);
        }
        // 处理后继节点
        if(pre != null && pre.getRight() == null) {
            // 让前驱节点的右指针指向当前节点
            pre.setRight(node);
            // 修改前驱节点的右指针类型
            pre.setRightType(1);
        }
        //!!! 每处理一个节点后,让当前节点是下一个节点的前驱节点
        pre = node;
        // 3、线索化右子树
        threadedNodes(node.getRight());
    }

    // 删除节点
    public void delNode(int no) {
        if(root != null) {
            // 如果只有一个root节点,这里立即判断root是不是就是要删除的节点
            if(root.getNo() == no) {
                root = null;
            } else {
                // 递归删除
                root.delNode(no);
            }
        } else {
            System.out.printf("空树,不能删除");
        }
    }

    // 前序遍历
    public void preOrder() {
        if(this.root != null) {
            this.root.preOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 中序遍历
    public void infixOrder() {
        if(this.root != null) {
            this.root.infixOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 后序遍历
    public void postOrder() {
        if(this.root != null) {
            this.root.postOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 前序遍历
    public HeroNode preOrderSearch(int no) {
        if(root != null) {
            return root.preOrdersearch(no);
        } else {
            return null;
        }
    }

    // 中序遍历
    public HeroNode infixOrderSearch(int no) {
        if(root != null) {
            return root.infixOrderSearch(no);
        } else {
            return null;
        }
    }

    // 后序遍历
    public HeroNode postOrderSearch(int no) {
        if(root != null) {
            return root.postOrderSearch(no);
        } else {
            return null;
        }
    }
}

// 创建HeroNode 节点
class HeroNode {
    private int no;
    private String name;
    private HeroNode left;// 默认为空
    private HeroNode right;// 默认为空
    private int leftType; // 0指向左子树  1指向前驱节点
    private int rightType;// 0指向右子树  1指向后驱节点

    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    public int getLeftType() {
        return leftType;
    }

    public void setLeftType(int leftType) {
        this.leftType = leftType;
    }

    public int getRightType() {
        return rightType;
    }

    public void setRightType(int rightType) {
        this.rightType = rightType;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    // 递归删除节点
    public void delNode(int no) {
        if(this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        if(this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        if(this.left != null) {
            this.left.delNode(no);
        }
        if(this.right != null) {
            this.right.delNode(no);
        }
    }

    // 编写前序遍历的方法
    public void preOrder() {
        System.out.println(this);// 先输出父节点
        // 递归向左子树前序遍历
        if(this.left != null) {
            this.left.preOrder();
        }
        // 递归向右子树前序遍历
        if(this.right != null) {
            this.right.preOrder();
        }
    }
    // 中序遍历
    public void infixOrder() {
        // 递归向左子树中序遍历
        if(this.left != null) {
            this.left.infixOrder();
        }
        // 输出父节点
        System.out.println(this);
        // 递归向右子树中序遍历
        if(this.right != null) {
            this.right.infixOrder();
        }
    }
    // 后序遍历
    public void postOrder() {
        // 递归向左子树后序遍历
        if(this.left != null) {
            this.left.postOrder();
        }
        // 递归向右子树后序遍历
        if(this.right != null) {
            this.right.postOrder();
        }
        // 输出父节点
        System.out.println(this);
    }

    // 前序遍历查找
    public HeroNode preOrdersearch(int no) {
        // 比较当前节点是不是
        if(this.no == no) {
            return this;
        }
        // 1、判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
        // 2、如果左递归前序查找,找到节点,则返回
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.preOrdersearch(no);
        }
        if(resNode != null) { // 说明我们左子树找到
            return resNode;
        }
        // 1、左递归前序查找,找到节点,则返回,否继续判断
        // 2、当前节点的右子节点是否为空,如果不空,则继续向右递归前序查找
        if(this.right != null) {
            resNode = this.right.preOrdersearch(no);
        }
        return resNode;
    }

    // 中序遍历查找
    public HeroNode infixOrderSearch(int no) {
        // 判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.infixOrderSearch(no);
        }
        if(resNode != null) {
            return resNode;
        }
        // 如果找到,则返回,如果没有找到,就和当前节点比较,如果是则返回当前节点
        if(this.no == no) {
            return this;
        }
        // 否则继续进行右递归的中序查找
        if(this.right != null) {
            resNode = this.right.infixOrderSearch(no);
        }
        return resNode;
    }

    // 后序遍历查找
    public HeroNode postOrderSearch(int no) {
        // 判断当前节点的左子节点是否为空,如果不为空,则递归后序查找
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.postOrderSearch(no);
        }
        if(resNode != null) { // 说明在左子树找到
            return resNode;
        }
        // 如果左子树没有找到,则向右子树递归进行后序遍历查找
        if(this.right != null) {
            resNode = this.right.postOrderSearch(no);
        }
        if(resNode != null) {
            return resNode;
        }
        // 如果左右子树都没有找到,就比较当前节点是不是
        if(this.no == no) {
            return this;
        }
        return resNode;
    }
}
2.5.7.3 遍历线索化二叉树

**说明:**对前面的中序线索化的二叉树,进行遍历
**分析:**因为线索化后,各个节点的指向有变化,因为==原来的遍历方式不能使用,==这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。遍历的次序应当和中序遍历保持一致。

package com.sss.tree.ThreadedBinaryTree;

public class ThreadedBinaryTreeDemo {
    public static void main(String[] args) {
        // 测试一把中序线索二叉树
        HeroNode root = new HeroNode(1, "tom");
        HeroNode node2 = new HeroNode(3, "jack");
        HeroNode node3 = new HeroNode(6, "smith");
        HeroNode node4 = new HeroNode(8, "mary");
        HeroNode node5 = new HeroNode(10, "king");
        HeroNode node6 = new HeroNode(14, "dim");

        root.setLeft(node2);
        root.setRight(node3);
        node2.setLeft(node4);
        node2.setRight(node5);
        node3.setLeft(node6);

        // 测试中序线索化
        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
        threadedBinaryTree.setRoot(root);
        threadedBinaryTree.threadedNodes();

        // 测试
        HeroNode left = node5.getLeft();
        HeroNode right = node5.getRight();
        System.out.println("10号节点的前驱节点是" + left);
        System.out.println("10号节点的后继节点是" + right);

        System.out.println("使用线索化的方式遍历 线索化二叉树");
        threadedBinaryTree.threadedList(); 

    }
}


// 定义ThreadBinaryTree 实现了线索化功能的二叉树
class ThreadedBinaryTree {
    private HeroNode root;

    // 为了实现线索化,需要创建给指向相当节点的前驱节点的指针
    // 在递归进行线索化时,pre总是保留前一个节点
    private HeroNode pre;

    public void setRoot(HeroNode root) {
        this.root = root;
    }

    // 遍历线索化二叉树的方法
    public void threadedList() {
        // 定义一个变量,存储当前遍历的节点,从root开始
        HeroNode node = root;
        while (node != null) {
            // 循环找到leftType = 1 的节点,第一个找到就是8节点
            // 后面随着遍历而变化,因为当leftType = 1时,说明该节点是按照线索化处理后的有效节点
            while (node.getLeftType() == 0) {
                node = node.getLeft();
            }
            // 打印当前这个节点
            System.out.println(node);
            // 如果当前节点的右指针指向的是后继节点就一直输出
            while (node.getRightType() == 1) {
                node = node.getRight();
                System.out.println(node);
            }
            // 替换这个遍历的节点
            node = node.getRight();
        }
    }



    // 重载一把
    public void threadedNodes() {
        this.threadedNodes(root);
    }


    // 编写对二叉树进行中序线索化的方法

    /**
     *
     * @param node 就是当前需要线索化的节点
     */
    public void threadedNodes(HeroNode node) {
        if(node == null) {
            return;
        }
        // 1、先线索化左子树
        threadedNodes(node.getLeft());
        // 2、线索化当前节点
        if(node.getLeft() == null) {
            // 让当前节点的左指针指向前一个节点
            node.setLeft(pre);
            // 修改当前节点的左指针类型,指向前驱节点
            node.setLeftType(1);
        }
        // 处理后继节点
        if(pre != null && pre.getRight() == null) {
            // 让前驱节点的右指针指向当前节点
            pre.setRight(node);
            // 修改前驱节点的右指针类型
            pre.setRightType(1);
        }
        //!!! 每处理一个节点后,让当前节点是下一个节点的前驱节点
        pre = node;
        // 3、线索化右子树
        threadedNodes(node.getRight());
    }

    // 删除节点
    public void delNode(int no) {
        if(root != null) {
            // 如果只有一个root节点,这里立即判断root是不是就是要删除的节点
            if(root.getNo() == no) {
                root = null;
            } else {
                // 递归删除
                root.delNode(no);
            }
        } else {
            System.out.printf("空树,不能删除");
        }
    }

    // 前序遍历
    public void preOrder() {
        if(this.root != null) {
            this.root.preOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 中序遍历
    public void infixOrder() {
        if(this.root != null) {
            this.root.infixOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 后序遍历
    public void postOrder() {
        if(this.root != null) {
            this.root.postOrder();
        } else {
            System.out.println("二叉树为空,无法遍历");
        }
    }

    // 前序遍历
    public HeroNode preOrderSearch(int no) {
        if(root != null) {
            return root.preOrdersearch(no);
        } else {
            return null;
        }
    }

    // 中序遍历
    public HeroNode infixOrderSearch(int no) {
        if(root != null) {
            return root.infixOrderSearch(no);
        } else {
            return null;
        }
    }

    // 后序遍历
    public HeroNode postOrderSearch(int no) {
        if(root != null) {
            return root.postOrderSearch(no);
        } else {
            return null;
        }
    }
}

// 创建HeroNode 节点
class HeroNode {
    private int no;
    private String name;
    private HeroNode left;// 默认为空
    private HeroNode right;// 默认为空
    private int leftType; // 0指向左子树  1指向前驱节点
    private int rightType;// 0指向右子树  1指向后驱节点

    public HeroNode(int no, String name) {
        this.no = no;
        this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public HeroNode getLeft() {
        return left;
    }

    public void setLeft(HeroNode left) {
        this.left = left;
    }

    public HeroNode getRight() {
        return right;
    }

    public void setRight(HeroNode right) {
        this.right = right;
    }

    public int getLeftType() {
        return leftType;
    }

    public void setLeftType(int leftType) {
        this.leftType = leftType;
    }

    public int getRightType() {
        return rightType;
    }

    public void setRightType(int rightType) {
        this.rightType = rightType;
    }

    @Override
    public String toString() {
        return "HeroNode{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }

    // 递归删除节点
    public void delNode(int no) {
        if(this.left != null && this.left.no == no) {
            this.left = null;
            return;
        }
        if(this.right != null && this.right.no == no) {
            this.right = null;
            return;
        }
        if(this.left != null) {
            this.left.delNode(no);
        }
        if(this.right != null) {
            this.right.delNode(no);
        }
    }

    // 编写前序遍历的方法
    public void preOrder() {
        System.out.println(this);// 先输出父节点
        // 递归向左子树前序遍历
        if(this.left != null) {
            this.left.preOrder();
        }
        // 递归向右子树前序遍历
        if(this.right != null) {
            this.right.preOrder();
        }
    }
    // 中序遍历
    public void infixOrder() {
        // 递归向左子树中序遍历
        if(this.left != null) {
            this.left.infixOrder();
        }
        // 输出父节点
        System.out.println(this);
        // 递归向右子树中序遍历
        if(this.right != null) {
            this.right.infixOrder();
        }
    }
    // 后序遍历
    public void postOrder() {
        // 递归向左子树后序遍历
        if(this.left != null) {
            this.left.postOrder();
        }
        // 递归向右子树后序遍历
        if(this.right != null) {
            this.right.postOrder();
        }
        // 输出父节点
        System.out.println(this);
    }

    // 前序遍历查找
    public HeroNode preOrdersearch(int no) {
        // 比较当前节点是不是
        if(this.no == no) {
            return this;
        }
        // 1、判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
        // 2、如果左递归前序查找,找到节点,则返回
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.preOrdersearch(no);
        }
        if(resNode != null) { // 说明我们左子树找到
            return resNode;
        }
        // 1、左递归前序查找,找到节点,则返回,否继续判断
        // 2、当前节点的右子节点是否为空,如果不空,则继续向右递归前序查找
        if(this.right != null) {
            resNode = this.right.preOrdersearch(no);
        }
        return resNode;
    }

    // 中序遍历查找
    public HeroNode infixOrderSearch(int no) {
        // 判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.infixOrderSearch(no);
        }
        if(resNode != null) {
            return resNode;
        }
        // 如果找到,则返回,如果没有找到,就和当前节点比较,如果是则返回当前节点
        if(this.no == no) {
            return this;
        }
        // 否则继续进行右递归的中序查找
        if(this.right != null) {
            resNode = this.right.infixOrderSearch(no);
        }
        return resNode;
    }

    // 后序遍历查找
    public HeroNode postOrderSearch(int no) {
        // 判断当前节点的左子节点是否为空,如果不为空,则递归后序查找
        HeroNode resNode = null;
        if(this.left != null) {
            resNode = this.left.postOrderSearch(no);
        }
        if(resNode != null) { // 说明在左子树找到
            return resNode;
        }
        // 如果左子树没有找到,则向右子树递归进行后序遍历查找
        if(this.right != null) {
            resNode = this.right.postOrderSearch(no);
        }
        if(resNode != null) {
            return resNode;
        }
        // 如果左右子树都没有找到,就比较当前节点是不是
        if(this.no == no) {
            return this;
        }
        return resNode;
    }
}

2.6 堆排序

2.6.1 基本介绍

1)堆排序是利用堆这种数据结构而设计的一种排序算法,对排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
2)堆是具有以下性质的完全二叉树:每个节点的值都大于等于其左右孩子节点的值,称为大顶堆,注意:没有要求节点的左孩子节点大于右孩子节点的值。
3) 每个节点的值都小于或等于其左右孩子节点的值。称为小顶堆。
大顶堆特点:arr[i] >= arr[i * 2 + 1] && arr[i] >= arr[i*2 + 2]// i 对应第几个节点,i从0开始编号
小顶堆特点:arr[i] <= arr[i * 2 + 1] && arr[i] <= arr[i*2 + 2]// i 对应第几个节点,i从0开始编号

2.6.2 基本思路

1)将无序序列构建成一个堆,根据升降序需求选择大顶堆或小顶堆
2)将堆顶元素与末尾元素交换,将最大元素“沉”到数组末端
3)重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值