数据结构与算法 Java

数据data结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构。学好数据结构可以编写出更加漂亮,更加有效率的代码。

程序 = 数据结构 + 算法

一、数据结构

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

线性结构

        线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系

        线性结构有两种不同的存储结构,即顺序存储结构链式存储结构

  •                 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的
  •                 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据
    •                 元素以及相邻元素的地址信息

        线性结构常见的有:数组、队列、链表和栈

非线性结构:二维数组,多维数组,广义表,树结构,图结构

1. 数组

1.1 稀疏数组

实际需求:编写五子棋程序时,有存盘和续上盘的功能要求

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

基本介绍

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

稀疏数组的处理方法是:

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

稀疏数组举例说明

应用实例

  1. 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等) (如果直接存盘,class.data文件很大,尤其是大的二维数组(比如:地图或者棋盘),所以采用稀疏数组进行优化。
  2. 把稀疏数组存盘,并且可以从新恢复原来的二维数组数
  3. 代码实现
public class SparseArray {
    public static void main(String[] args) {
        int[][] sourceArr = new int[6][7];
        sourceArr[0][3] = 22;
        sourceArr[0][6] = 15;
        sourceArr[1][1] = 11;
        sourceArr[1][5] = 17;
        sourceArr[2][3] = -6;
        sourceArr[3][5] = 39;
        sourceArr[4][0] = 91;
        sourceArr[5][2] = 28;
        System.out.println("==========原始数组为==========");
        for (int i = 0; i < sourceArr.length; i++) {
            for (int j = 0; j < sourceArr[i].length; j++) {
                System.out.print(sourceArr[i][j] + "\t");
            }
            System.out.println();
        }
//        统计原始数组中非0 数据:
        int count = 0;
        for (int i = 0; i < sourceArr.length; i++) {
            for (int j = 0; j < sourceArr[i].length; j++) {
                if (sourceArr[i][j] != 0){
                    count++;
                }
            }
        }
//        创建稀疏数组:
        int[][] sparseArray = new int[count + 1][3];
        int row = 1;
        sparseArray[0][0] = sourceArr.length;
        sparseArray[0][1] = sourceArr[0].length;
        sparseArray[0][2] = count;
        for (int i = 0; i < sourceArr.length; i++) {
            for (int j = 0; j < sourceArr[i].length; j++) {
                if (sourceArr[i][j] != 0){
                    sparseArray[row][0] = i;
                    sparseArray[row][1] = j;
                    sparseArray[row][2] = sourceArr[i][j];
                    row++;
                }

            }
        }
        System.out.println("==========稀疏数组为==========");
        for (int i = 0; i < sparseArray.length; i++) {
            for (int j = 0; j < sparseArray[i].length; j++) {
                System.out.print(sparseArray[i][j] + "\t");
            }
            System.out.println();
        }
//        还原为原始数组:
        int[][] aimArray = new int[sparseArray[0][0]][sparseArray[0][1]];
        for (int i = 1; i < sparseArray.length; i++) {
            aimArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
        }
        System.out.println("==========还原后的原始数组为==========");
        for (int i = 0; i < aimArray.length; i++) {
            for (int j = 0; j < aimArray[i].length; j++) {
                System.out.print(aimArray[i][j] + "\t");
            }
            System.out.println();
        }
    }
}

2 队列

应用场景:银行排队

队列是一个有序列表,可以用数组或是链表来实现。

遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出

示意图:(使用数组模拟队列示意图)

2.1 数组模拟单向队列

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

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

当将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:

(1)将尾指针往后移:rear+1 , 当front == rear 【空】

(2)若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear所指的数组元素中,否则无法存入数据。 rear  == maxSize - 1[队列满]

注:1. rear 是队列最后[含]    2. front 是队列最前元素[不含]

队列具备的功能

  •         出队列操作getQueue
    •         显示队列的情况showQueue
      •         查看队列头元素headQueue
        •         退出系统exit
public class Queue {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        CreateQueue queue = new CreateQueue(3);

        boolean flag = true;
        while (flag){
//            System.out.println("========================");
            System.out.println("请给出指令:");
//            System.out.println("========================");
            String key = scanner.nextLine();
            switch (key){
                case "get":
                    queue.getNum();
                    break;
                case "add":
                    System.out.println("请输入要加入的数据");
                    int num = scanner.nextInt();
                    queue.add(num);
                    break;
                case "show":
                    queue.show();
                    break;
                case "head":
                    queue.head();
                    break;
                case "exit":
                    scanner.close();
                    flag = false;
                    break;
                default:
                    break;
            }
        }
    }
}
class CreateQueue{
    int maxsize;
    int front;
    int rear;
    int[] queue;

    public CreateQueue(int maxsize) {
        this.maxsize = maxsize;
        front = -1;
        rear = -1;
        queue = new int[maxsize];
    }

    public boolean isFull(){
        return rear == maxsize - 1;
    }
    public boolean isEmpty(){
        return front == rear;
    }
    public void add(int num){
        if (isFull()){
            System.out.println("队列已满,无法加入");
            return;
        }
        rear++;
        queue[rear] = num;
    }
    public void getNum(){
        if (isEmpty()){
            System.out.println("队列已空,无法取出。");
            return;
        }
        System.out.println("取出" + queue[front+1]);
        front++;
    }
    public void head(){
        if (isEmpty()){
            System.out.println("队列空,无数据!");
            return;
        }
        front++;
        System.out.println("队列头数据为:" + queue[front]);
    }
    public void show(){
        if (isEmpty()){
            System.out.println("队列为空!");
            return;
        }
        for (int i = front + 1; i < rear +1; i++) {
            System.out.println("第" + i + "个位置元素为:" + queue[i]);
        }
    }
}

 问题分析并优化:

        1)目前数组使用一次就不能再用,没有达到复用的效果

        2)将这个数组改成环形的队列,取模:%

2.2 数组模拟环形队列

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

分析说明:

        1)尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的时候需要注意 (rear + 1) % maxSize == front 满]

        2)rear == front [空]

        3)测试示意图:

4)思路:
  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;
public class CircleQueue {
    public static void main(String[] args) {

        CreateCircleQueue queue = new CreateCircleQueue(4);
        Scanner scanner = new Scanner(System.in);
        boolean flag = true;
        String key;
        while (flag){

            System.out.println("请输入指令:");
            key = scanner.nextLine();
            switch (key){
                case "show":
                    queue.show();
                    break;
                case "add":
                    int num = scanner.nextInt();
                    queue.add(num);
                    break;
                case "get":
                    try {
                        int get = queue.getQueue();
                        System.out.println(get);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case "head":
                    try {
                        int head = queue.getHead();
                        System.out.println(head);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                case "exit":
                    scanner.close();
                    flag = false;
                    break;
            }
        }
    }
}
class CreateCircleQueue{
    int maxsize;
    int front;
    int rear;
    int[] queue;

    public CreateCircleQueue(int maxsize) {
        this.maxsize = maxsize;
        front = 0;
        rear = 0;
        queue = new int[maxsize];
    }
    public boolean isEmpty(){
        return rear == front;
    }
    public boolean isFull(){
        return (rear + 1 - front ) % maxsize == 0;
    }
    public void add(int num){
        if (isFull()){
            System.out.println("队列已满,无法加入");
            return;
        }
        queue[rear] = num;
        rear = (rear + 1)% maxsize;

    }
    public int getQueue(){
        if (isEmpty()){
            throw new RuntimeException("队列为空,无法取出。");
        }

        int temp = queue[front];
        front = (front+1) % maxsize;
        return temp;
    }
    public int getHead(){
        if (isEmpty()){
            throw new RuntimeException("队列为空,无法得到头数据!");
        }
        return queue[front];
    }
    public int size(){
        return (rear + maxsize - front) % maxsize;
    }
    public void show(){
        if (isEmpty()){
            System.out.println("队列为空");
            return;
        }
        for (int i = front; i < front + size(); i++) {
            System.out.println("第" + i + "个元素为:" + queue[i % maxsize]);
        }
    }
}

3. 链表

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

小结:

1)链表是以节点的方式来存储,是链式存储

2)每个节点包含 data 域, next 域:指向下一个节点.

3)如图:发现链表的各个节点不一定是连续存储.

4)链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定

3.1 单链表

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

使用带head头的单向链表完成增删改查操作:

(头结点:① 无具体数据;② 表示单链表头)

第一种方法在添加节点时,直接添加到链表的尾部

添加辅助变量遍历是因为head节点不能动,只能动辅助结点。 

第二种方式在添加节点时,根据序号大小将节点插入到指定位置(如果有这个序号,则添加失败,并给出提示)

删除功能点:

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

1) 求单链表中有效节点的个数
2) 查找单链表中的倒数第 k 个结点 新浪面试题
3) 单链表的反转 腾讯面试题,有点难度

步骤2-1:将指针从当前节点移向下一节点;

步骤2-2:将当前节点的下一节点(temp.next)指向新链表最前端;

步骤2-3:将当前节点连接到新链表头结点reverseHead后(新链表头结点的next指向当前节点),形成新链表。

4) 从尾到头打印单链表 百度,要求方式 1 :反向遍历 。 方式 2 Stack
5) 合并两个有序的单链表,合并之后的链表依然有序 课后练习 .】

代码实现:

public class SingleLinkedList {
    public static void main(String[] args) {
        Node1 node1 = new Node1(1, "唐僧");
        Node1 node2 = new Node1(2, "孙悟空");
        Node1 node3 = new Node1(3, "猪八戒");
        Node1 node4 = new Node1(4, "沙僧");
        Node1 node5 = new Node1(5, "白龙马");
        SingleList singleList = new SingleList();
//        singleList.add(node1);
//        singleList.add(node2);
//        singleList.add(node4);
//        singleList.add(node5);
        singleList.addByOrd(node3);
        singleList.addByOrd(node1);
        singleList.addByOrd(node5);
        singleList.addByOrd(node4);
        singleList.addByOrd(node2);
        node3 = new Node1(3, "悟能");
        singleList.update(node3);
        singleList.list();
        System.out.println("====================");
//        singleList.del(2);
//        singleList.list();
        System.out.println(cnt(singleList.getHead()));
        System.out.println("=======================");
        reverse(singleList.getHead());
        singleList.list();
//        Node1 node = getNode(2, singleList.getHead());
//        System.out.println(node);
    }
    public static int cnt(Node1 head){
        int count = 0;
        Node1 temp = head;
        while (true){
            if (temp.next == null){
                break;
            }
            temp = temp.next;
            count++;
        }
        return count;
    }
    public static Node1 getNode(int k, Node1 head){
        Node1 temp = head;
//        Node1 temp2 = head;
        int count = 0;
        while (true){
            if (count == cnt(head) - k + 1){
                break;
            }
            count++;
            temp = temp.next;
//            if (count >= k){
//                temp2 = temp2.next;
//            }

        }
        return temp;
    }
    public static void reverse(Node1 head){
        Node1 reHead = new Node1(0, null);
        Node1 temp = head.next;
        Node1 cur;
        while (temp != null){
            cur = temp.next;
            temp.next = reHead.next;
            reHead.next = temp;
            temp = cur;

        }
        head.next = reHead.next;
    }

}
class Node1{
    int no;
    String name;
    Node1 next;

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

    @Override
    public String toString() {
        return "Node1{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}
class SingleList{
    Node1 head = new Node1(0, null);

    public Node1 getHead() {
        return head;
    }



    public void add(Node1 node){
        if (head.next == null){
            head.next = node;
        }else {
            Node1 temp = head;
            while (true) {
                if (temp.next == null) {
                    break;
                }
                temp = temp.next;
            }
            temp.next = node;
        }
    }
    public void addByOrd(Node1 node){
        Node1 temp = head;
        boolean flag = false;
        while (true){
            if (temp.next == null){
                flag = true;
                break;
            }
            if (node.no < temp.next.no){
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag){
            node.next = temp.next;
            temp.next = node;
        }
    }
//    改
    public void update(Node1 node){
        Node1 temp = head;
        while (true){
            if (temp.next == null){
                break;
            }
            if (temp.next.no == node.no){
//                node.next = temp.next.next;
//                temp.next = node;
                temp.next.name = node.name;
                break;
            }
            temp = temp.next;
        }
    }
//    删除
    public void del(int no){
        Node1 temp = head;
        if (temp.next == null){
            System.out.println("空链表");
            return;
        }
        while (true){
            if (temp.next == null){
                break;
            }
            if (temp.next.no == no){
                temp.next = temp.next.next;
            }
            temp = temp.next;
        }
    }
    public void list(){
        if (head.next == null){
            System.out.println("空链表");
            return;
        }
        Node1 temp = head.next;
        while (true){
            if (temp == null){
                break;
            }
            System.out.println(temp);
            temp = temp.next;
        }
    }
}

3.2 双向链表

单向链表的缺点分析:

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

删除的步骤4:

有一个前提条件:

if (temp.next != null) {temp.next.pre = temp.pre}

public class DoubleLinkedList {
    public static void main(String[] args) {
        Node node1 = new Node(1, "唐僧");
        Node node2 = new Node(2, "孙悟空");
        Node node3 = new Node(3, "猪八戒");
        Node node4 = new Node(4, "沙僧");
        Node node5 = new Node(5, "白龙马");
        DoubleList doubleList = new DoubleList();
        doubleList.add(node1);
        doubleList.add(node2);
        doubleList.add(node3);
        doubleList.add(node4);
        doubleList.add(node5);
        doubleList.list();
        System.out.println("===========================");
        node3 = new Node(3, "悟净");
        doubleList.update(node3);
        doubleList.list();
        System.out.println("===========================");
        doubleList.del(5);
        doubleList.list();
    }
}
class Node{
    int no;
    String name;
    Node next;
    Node pre;

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

    @Override
    public String toString() {
        return "Node{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}
class DoubleList{
    Node head = new Node(0, null);
    public void add(Node node){
        Node temp = head;
        while (true){
            if (temp.next == null){
                break;
            }
            temp = temp.next;
        }
        temp.next = node;
        node.pre = temp;
    }
    public void del(int no){
        Node temp = head;
        boolean flag = false;
        while (true){
            if (temp.no == no){
                flag = true;
                break;
            }
            if (temp.next == null){
                break;
            }
            temp = temp.next;
        }
        if (flag){
            temp.pre.next = temp.next;
            if (temp.next != null) {
                temp.next.pre = temp.pre;
            }
        }else {
            System.out.println("不存在该节点");
        }
    }
    public void update(Node node){
        Node temp = head;
        while (true){
            if (temp.next == null){
                break;
            }
            if (temp.next.no == node.no){
                temp.next.name = node.name;
                break;
            }
            temp = temp.next;
        }
    }
    public void list(){
        Node temp = head.next;
        while (true){
            if (temp == null){
                break;
            }
            System.out.println(temp);
            temp = temp.next;
        }
    }
}

3.3 约瑟夫单向环形链表

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

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

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

创建环形链表: 

节点出圈:

环形链表出圈:

有num个结点,从start位置的节点开始,每隔count个的节点被取出;

  1. 创建一个辅助指针helper指向环形链表的最后节点;
  2. 取count为的节点前,先让first和helper移动(start-1)次,即:移动到开始的前一个位置;
  3. 开始数count次时,让first和helper指针同时移动(count-1)次;
  4. 将first指向的节点出圈;

代码实现: 

public class CircleLinkedList {
    public static void main(String[] args) {
        CircleList circleList = new CircleList();
        circleList.add(10);
        circleList.show();
        System.out.println();
//        circleList.outQueueGame(5, 3);
        System.out.println("========================");
        circleList.getOut(3, 5);
    }
}
class CircleNode{
    int no;
    CircleNode next;

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

    @Override
    public String toString() {
        return "CircleNode{" +
                "no=" + no +
                '}';
    }
}
class CircleList{
    CircleNode first;

    public void add(int num){
        CircleNode temp = first;
        for (int i = 1; i <= num; i++) {
            CircleNode node = new CircleNode(i);
            if (i == 1){
                first = node;
                node.next = first;
                temp = first;
            }else {
                temp.next = node;
                node.next = first;
                temp = node;
            }
        }
    }
    public void show(){
        CircleNode temp = first;
        while (true){
            System.out.println(temp);
            if (temp.next == first){
                break;
            }
            temp = temp.next;
        }
    }
    public void outQueueGame(int gap, int start){
        CircleNode temp = first;
        while (true){
            if (temp.next == first){
                break;
            }
            temp = temp.next;
        }
        for (int i = 0; i < start - 1; i++) {
            first = first.next;
            temp = temp.next;
        }
        while (true) {
            if (temp == first){
                break;
            }
            for (int i = 0; i < gap - 1; i++) {
                first = first.next;
                temp = temp.next;
            }
            System.out.print(first.no + "\t");
            first = first.next;
            temp.next = first;
        }
        System.out.println("遗留下的节点为:" + first.no);
    }
    public void getOut(int start, int gap){
        CircleNode helper = first;
        while (true){
            if (helper.next == first){
                break;
            }
            helper = helper.next;
        }
        for (int i = 0; i < start - 1; i++) {
            first = first.next;
            helper = helper.next;
        }
        while (true){
            if (helper == first){
                break;
            }
            for (int i = 0; i < gap - 1; i++) {
                first = first.next;
                helper = helper.next;
            }
            System.out.print(first.no + "\t");
            first = first.next;
            helper.next = first;
        }
        System.out.println("最后的节点为:" + first.no);
    }
}

4. 栈

栈的实际需求:请输入一个表达式

计算式:[7*2*2-5+1-5+3-3] 点击计算如下图

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

栈的介绍:

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

栈的应用场景:

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

4.1 数组模拟栈

1) 数组模拟栈 的使用,由于栈是一种有序列表, 当然可以使用数组的结构来储存栈的数据内容, 下面我们就用数组模拟栈的 出栈 入栈 等操作。
2) 实现思路分析 , 并画出示意图
代码实现:
public class StackDemo {
    public static void main(String[] args) {
        Stack stack = new Stack(4);
        Scanner scanner = new Scanner(System.in);
        String key;
        boolean flag = true;
        while (flag){
            System.out.println("请输入指令");
            key = scanner.next();
            switch (key){
                case "push":
                    System.out.println("请输入入栈数字:");
                    int num = scanner.nextInt();
                    stack.push(num);
                    break;
                case "pop":
                    int pop = stack.pop();
                    System.out.println("栈顶取出的元素为:" + pop);
                    break;
                case "peek":
                    int peek = stack.peek();
                    System.out.println("栈顶元素为:" + peek);
                    break;
                case "list":
                    stack.list();
                    break;
                case "exit":
                    scanner.close();
                    flag = false;
                    break;
            }
        }
    }
}
class Stack{
    int[] arr;
    int maxsize;
    int top;

    public Stack(int maxsize) {
        this.maxsize = maxsize;
        arr = new int[maxsize];
        this.top = -1;
    }
    public void push(int num){
        if (top == maxsize - 1){
            System.out.println("栈已满,无法加入");
            return;
        }
        top++;
        arr[top] = num;

    }
    public int pop(){
        if (top == -1){
            throw new RuntimeException("栈已空,无法取出。");
        }
        int temp = arr[top];
        top--;
        return temp;
    }
    public int peek(){
        if (top == -1){
            throw new RuntimeException("栈已经空,无头元素。");
        }
        return arr[top];
    }
    public void list(){
        if (top == -1){
            System.out.println("空栈");
            return;
        }
        for (int i = top; i >= 0; i--) {
            System.out.println("第 " + i + " 个位置为:" + arr[i]);
        }
    }
}

4.2 栈的应用

(1)中缀表达式(综合计算器)
1) 中缀表达式就是 常见的运算表达式 ,如 (3+4)×5-6
2) 中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作 ( 前面我们讲的案例就能看的这个问题 ) ,因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作 ( 一般转成后缀表达式 .)

中缀表达式:

  1. 通过一个index值来遍历表达式;
  2. 如果是数字,直接入栈(通过拼接字符串转为数字);
    • str += ch;
    • 扫描到了最后一位(index == expression.length() - 1),直接压入数字栈;
    • 未扫描到最后一位时,如果下一个符号是运算符,直接压入数字栈,并将数字串str置空;
  3. 如果扫描到的是一个符号
    • 若当前符号栈为空,这直接进符号栈;
    • 若符号栈有操作符,则进行比较:
      1. 若当前操作符优先级 <= 栈顶操作符优先级:需要从数字栈取出两个数,并从符号栈取出一个操作符进行运算,将得到的结果入数栈,当前扫描到的符号入符号栈。
      2. 若当前操作符优先级 > 栈中操作符优先级:直接将该符号入符号栈。
  4. 当表达式扫描完(index >= expression.length() ),顺序从数栈和符号栈中取出相应的数字和符号,并进行计算。符号栈为空时完成计算。
  5. 最后栈中只有一个数字,即表达式结果。

代码实现:

public class GenaralExpression {
    public static void main(String[] args) {
        String exp = "12/3*4+18-12+3";      //中缀表达式
        Stack<Character> operStack = new Stack<>();
        Stack<Integer> numStack = new Stack<>();
        int index = 0;
        String num = "";
        while (index < exp.length()){
            char c = exp.charAt(index);
            if ((c + "").matches("[0-9]")){
                num += c;
                if (index == exp.length() - 1){
                    numStack.push(Integer.parseInt(num));
                }else {
                    char ch = exp.charAt(index +1);
                    if ((ch + "").matches("[^0-9]")){
                        numStack.push(Integer.parseInt(num));
                        num = "";
                    }
                }
            }else {
                if (operStack.size() == 0){
                    operStack.push(c);
                }else {
                    if (prio(c) <= prio(operStack.peek())){
                        int num2 = numStack.pop();
                        int num1 = numStack.pop();
                        Character pop = operStack.pop();
                        int res = cal(num1, num2, pop);
                        numStack.push(res);
                        operStack.push(c);    ///
                    }else {
                        operStack.push(c);
                    }
                }
            }
            index++;
        }
        while (operStack.size() >0){
            int num2 = numStack.pop();
            int num1 = numStack.pop();
            Character pop = operStack.pop();
            int res = cal(num2, num1, pop);
            numStack.push(res);
        }
        System.out.println(numStack.peek());
    }
    public static int prio(char ope){
        if (ope == '+' || ope == '-'){
            return 1;
        }else if (ope == '/' || ope == '*'){
            return 2;
        }else {
            return 0;
        }
    }
    public static int cal(int num1, int num2, char ope){
        int res;
        if (ope == '+'){
            res = num1 + num2;
        }else if (ope == '-'){
            res = num1 - num2;
        } else if (ope == '*') {
            res = num1 * num2;
        }else {
            return num1 / num2;
        }
        return res;
    }
}
(2)前缀表达式
1) 前缀表达式又称波兰式, 前缀表达式的运算符位于操作数之前
2) 举例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6
前缀表达式的计算机求值

从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:

1)右至左扫描,将6543压入堆栈

2)遇到+运算符,因此弹出343为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈

3)接下来是×运算符,因此弹出75,计算出7×5=35,将35入栈

4)最后是-运算符,计算出35-6的值,即29,由此得出最终结果

(3)后缀的计算

后缀表达式

1) 后缀表达式又称 逆波兰表达式 , 与前缀表达式相似,只是运算符位于操作数之后
2) 中举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
3) 再比如 :

正常的表达式

逆波兰表达式

a+b

a b +

a+(b-c)

a b c - +

a+(b-c)*d

a b c d * +

a+d*(b-c)

a d b c - * +

a=1+3

a 1 3 + =

后缀表达式的计算机求值

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

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

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

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

具体步骤如下:

1) 初始化两个栈:运算符栈 s1 和储存中间结果的栈 s2(一般s2用列表代替);首先将中缀表达式字符串变为列表的形式(数字对应的字符编码48-57);
2) 从左至右扫描中缀表达式;
3) 遇到操作数时,将其压 s2
4) 遇到运算符时,比较其与 s1 栈顶运算符的优先级:
(1) 如果 s1 为空,或栈顶运算符为左括号“ (” ,则直接将此运算符入栈;
(2) 否则,若优先级比栈顶运算符的高,也将运算符压入 s1
(3)否则,将s1栈顶的运算符弹出并压入到 s2 中,本项加到s1,再次转到 (4-1) s1 中新的栈顶运算符相比较;
   

4)中的三步可以简化成:

While(s1.size() != 0 && 当前扫描到的运算符优先级 <= s1栈顶运算符){s2.add(s1.pop()); s1.push(item);}  其他直接压入s1;

5) 遇到括号时:
(1) 如果是左括号“ (” ,则直接压入 s1
(2)
如果是右括号“ )” ,则依次弹出 s1 栈顶的运算符,并压入 s2 ,直到遇到左括号为止,此时将这一对括号丢弃
6) 重复步骤 2 5 ,直到表达式的最右边
7) s1 中剩余的运算符依次弹出并压入 s2
8) 依次弹出 s2 中的元素并输出, 结果的逆序即为中缀表达式对应的后缀表达式

举例说明:

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

扫描到的元素

s2(栈底->栈顶)

s1 (栈底->栈顶)

说明

1

1

数字,直接入栈

+

1

+

s1为空,运算符直接入栈

(

1

+ (

左括号,直接入栈

(

1

+ ( (

同上

2

1 2

+ ( (

数字

+

1 2

+ ( ( +

s1栈顶为左括号,运算符直接入栈

3

1 2 3

+ ( ( +

数字

)

1 2 3 +

+ (

右括号,弹出运算符直至遇到左括号

×

1 2 3 +

+ ( ×

s1栈顶为左括号,运算符直接入栈

4

1 2 3 + 4

+ ( ×

数字

)

1 2 3 + 4 ×

+

右括号,弹出运算符直至遇到左括号

-

1 2 3 + 4 × +

-

-+优先级相同,因此弹出+,再压入-

5

1 2 3 + 4 × + 5

-

数字

到达最右端

1 2 3 + 4 × + 5 -

s1中剩余的运算符

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

代码实现:

public class Poland {
    public static void main(String[] args) {
        String exp = "0-3+2*(1+2*(0-4/(8-6)+7))";//"1+((2+3)*4)-5";    //"12/3*4+18-12+33/11*2";  //28
        List<String> srcList = getExpList(exp);
        System.out.println(srcList);
        Stack<String> opStack = new Stack<>();
        ArrayList<String> list = new ArrayList<>();
        for (String s : srcList) {
            if (s.matches("\\d+")){
                list.add(s);
            }else {
                if (s.equals("(")) {
                    opStack.push(s);
                } else if (s.equals(")")) {
                    while (!opStack.peek().equals("(")) {
                        list.add(opStack.pop());
                    }
                    opStack.pop();
                } else {
                    while (opStack.size() != 0 && prio(s) <= prio(opStack.peek())) {
                        list.add(opStack.pop());
                    }
                    opStack.push(s);

                }
            }
        }
        while (opStack.size() > 0){
            list.add(opStack.pop());
        }
        System.out.println(opStack);
        System.out.println(list);
        Stack<Integer> stack = new Stack<>();
        for (String s : list) {
            if (s.matches("\\d+")){
                stack.push(Integer.parseInt(s));
            }else {
                Integer num2 = stack.pop();
                Integer num1 = stack.pop();
                int res = cal(num1, num2, s);
                stack.push(res);
            }
        }
        System.out.println(stack.peek());

    }
    public static int cal(int num1, int num2, String ope){
        int res;
        if (ope.equals("+")){
            res = num1 + num2;
        }else if (ope.equals("-")){
            res = num1 - num2;
        } else if (ope.equals("*")) {
            res = num1 * num2;
        }else {
            return num1 / num2;
        }
        return res;
    }
    public static boolean isOp(String op){
        return op.equals("+") || op.equals("-") || op.equals("*") || op.equals("/");
    }
    public static int prio(String ope){
        if (ope.equals("+") || ope.equals("-")){
            return 1;
        }else if (ope.equals("*") || ope.equals("/")){
            return 2;
        }else {
            return 0;
        }
    }
    public static List<String> getExpList(String str){
        char[] chars = str.toCharArray();
        String num = "";
        int index = 0;
        ArrayList<String> list = new ArrayList<>();
        while (index < str.length()) {
            char c = str.charAt(index);
            if ((c + "").matches("[0-9]")){
                num += c;
                if (index == str.length() - 1){
                    list.add(num);
                }else {
                    if ((str.charAt(index + 1) + "").matches("[^0-9]")){
                        list.add(num);
                        num = "";
                    }
                }
            }else {
                list.add(c+"");
            }
            index++;
        }
        return list;
    }

}
(5)完整版逆波兰计算器
public class ReversePolishMultiCalc {

	 /**
     * 匹配 + - * / ( ) 运算符
     */
    static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";

    static final String LEFT = "(";
    static final String RIGHT = ")";
    static final String ADD = "+";
    static final String MINUS= "-";
    static final String TIMES = "*";
    static final String DIVISION = "/";

    /**
     * 加減 + -
     */
    static final int LEVEL_01 = 1;
    /**
     * 乘除 * /
     */
    static final int LEVEL_02 = 2;

    /**
     * 括号
     */
    static final int LEVEL_HIGH = Integer.MAX_VALUE;


    static Stack<String> stack = new Stack<>();
    static List<String> data = Collections.synchronizedList(new ArrayList<String>());

    /**
     * 去除所有空白符
     * @param s
     * @return
     */
    public static String replaceAllBlank(String s ){
        // \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
        return s.replaceAll("\\s+","");
    }

    /**
     * 判断是不是数字 int double long float
     * @param s
     * @return
     */
    public static boolean isNumber(String s){
        Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
        return pattern.matcher(s).matches();
    }

    /**
     * 判断是不是运算符
     * @param s
     * @return
     */
    public static boolean isSymbol(String s){
        return s.matches(SYMBOL);
    }

    /**
     * 匹配运算等级
     * @param s
     * @return
     */
    public static int calcLevel(String s){
        if("+".equals(s) || "-".equals(s)){
            return LEVEL_01;
        } else if("*".equals(s) || "/".equals(s)){
            return LEVEL_02;
        }
        return LEVEL_HIGH;
    }

    /**
     * 匹配
     * @param s
     * @throws Exception
     */
    public static List<String> doMatch (String s) throws Exception{
        if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
        if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");

        s = replaceAllBlank(s);

        String each;
        int start = 0;

        for (int i = 0; i < s.length(); i++) {
            if(isSymbol(s.charAt(i)+"")){
                each = s.charAt(i)+"";
                //栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
                if(stack.isEmpty() || LEFT.equals(each)
                        || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
                    stack.push(each);
                }else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
                    //栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
                    while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
                        if(calcLevel(stack.peek()) == LEVEL_HIGH){
                            break;
                        }
                        data.add(stack.pop());
                    }
                    stack.push(each);
                }else if(RIGHT.equals(each)){
                    // ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
                    while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
                        if(LEVEL_HIGH == calcLevel(stack.peek())){
                            stack.pop();
                            break;
                        }
                        data.add(stack.pop());
                    }
                }
                start = i ;    //前一个运算符的位置
            }else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
                each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
                if(isNumber(each)) {
                    data.add(each);
                    continue;
                }
                throw new RuntimeException("data not match number");
            }
        }
        //如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
        Collections.reverse(stack);
        data.addAll(new ArrayList<>(stack));

        System.out.println(data);
        return data;
    }

    /**
     * 算出结果
     * @param list
     * @return
     */
    public static Double doCalc(List<String> list){
        Double d = 0d;
        if(list == null || list.isEmpty()){
            return null;
        }
        if (list.size() == 1){
            System.out.println(list);
            d = Double.valueOf(list.get(0));
            return d;
        }
        ArrayList<String> list1 = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            list1.add(list.get(i));
            if(isSymbol(list.get(i))){
                Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
                list1.remove(i);
                list1.remove(i-1);
                list1.set(i-2,d1+"");
                list1.addAll(list.subList(i+1,list.size()));
                break;
            }
        }
        doCalc(list1);
        return d;
    }

    /**
     * 运算
     * @param s1
     * @param s2
     * @param symbol
     * @return
     */
    public static Double doTheMath(String s1,String s2,String symbol){
        Double result ;
        switch (symbol){
            case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
            case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
            case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
            case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
            default : result = null;
        }
        return result;

    }

    public static void main(String[] args) {
        //String math = "9+(3-1)*3+10/2";
        String math = "12.8 + (2 - 3.55)*4+10/5.0";
        try {
            doCalc(doMatch(math));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

5. 哈希表(散列表)

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

要求: 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)

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

添加时,保证按照id从低到高插入, 使用链表来实现哈希表, 该链表不带表头 [: 链表的第一个结点就存放雇员信息] 

代码实现:

public class HashTab {
    public static void main(String[] args) {
        HashTabLinkedList hashTabLinkedList = new HashTabLinkedList(6);
        Scanner scanner = new Scanner(System.in);
        String key;
        while (true){
            System.out.println("请输入指令:");
            key = scanner.next();
            switch (key){
                case "add":
                    System.out.println("请输入id:");
                    int id = scanner.nextInt();
                    System.out.println("请输入name:");
                    String name = scanner.next();
                    hashTabLinkedList.add(new Emp(id, name));
                    break;
                case "list":
                    hashTabLinkedList.list();
                    break;
                case "emp":
                    System.out.println("请输入要查询的id:");
                    int no = scanner.nextInt();
                    hashTabLinkedList.empsById(no);
                    break;
                case "exit":
                    scanner.close();
                    System.exit(0);
                    break;
            }
        }
    }
}
class HashTabLinkedList{
    EmpLinkedList[] empArrays;
    int size;

    public HashTabLinkedList(int size) {
        this.size = size;
        empArrays = new EmpLinkedList[size];
        for (int i = 0; i < size; i++) {
            empArrays[i] = new EmpLinkedList();             /
        }
    }
    public void add(Emp emp){
        empArrays[linkedListNo(emp.no)].add(emp);
    }
    public void list(){
        for (int i = 0; i < size; i++) {
            System.out.print("链表" + (i + 1) + "为:");
            empArrays[i].list(i);
            System.out.println();
        }
    }
    public void empsById(int no){
        Emp emp = empArrays[linkedListNo(no)].empById(no);
        if (emp == null){
            System.out.println("在哈希表中,未找到该雇员");
        }else {
            System.out.println("在第" + linkedListNo(no) + "条链表找到:" + emp);
        }
    }
    public int linkedListNo(int no){
        return no % size;
    }
}
class EmpLinkedList{
    Emp head;
    public void add(Emp emp){
        Emp temp = head;
        if (head == null){
            head = emp;
            return;
        }
        while (true){
            if (temp.next == null){
                break;
            }
            temp = temp.next;
        }
        temp.next = emp;
    }
    public void list(int no){
        if (head == null){
             System.out.println("链表" + ( no + 1) + "为空");
             return;
        }
        Emp temp = head;
        while (true){
            System.out.print("-->" + temp);
            if (temp.next == null){
                break;
            }
            temp = temp.next;
        }
    }
    public Emp empById(int no){
        if (head == null){
            throw new RuntimeException("不存在该员工");
        }
        Emp temp = head;
        while (true){
            if (temp.no == no){
                break;
            }
            if (temp.next == null){
                temp = null;
                break;
            }
            temp =temp.next;
        }
        return temp;
    }
}
class Emp{
    int no;
    String name;
    Emp next;

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

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

6. 树

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

1)  数组存储方式的分析
优点:通过下标方式访问元素,速度快。 对于有序数组 ,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值 ( 按一定顺序 ) 会整体移动 ,效率较低 [ 示意图 ]
2)  链式存储方式的分析
优点:在一定程度上对数组存储方式有优化 ( 比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好 )
缺点:在进行检索时,效率仍然较低,比如 ( 检索某个值,需要从头节点开始遍历 ) 示意图
3)  存储方式的分析
能提高数据 存储,读取 的效率 比如利用 二叉排序树 (Binary Sort Tree) ,既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度
案例 : [7, 3, 10, 1, 5, 9, 12]

树的常用术语(结合示意图理解):

1) 节点
2) 根节点
3) 父节点
4) 子节点
5) 叶子节点 ( 没有子节点的节点 )
6) 节点的权 ( 节点值 )
7) 路径 ( root 节点找到该节点的路线 )
8)
9) 子树
10) 树的高度 ( 最大层数 )
11) 森林 : 多颗子树构成森林

6.1 二叉树

1) 树有很多种,每个节点 最多只能有两个子节点 的一种形式称为二叉树。
2) 二叉树的子节点分为左节点和右节点。

3) 如果该二叉树的 所有叶子节点都在最后一层 ,并且结点总数 = 2^n -1 , n 为层数,则我们称为满二叉树。

4) 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。

二叉树遍历的说明

使用前序中序后序对下面的二叉树进行遍历.

前序遍历: 先输出父节点,再遍历左子树和右子树

中序遍历: 先遍历左子树,再输出父节点,再遍历右子树

后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点

小结: 看输出父节点的顺序,就确定是前序,中序还是后序

二叉树-查找指定节点

二叉树-删除节点

如果删除的节点是叶子节点,则删除该节点

如果删除的节点是非叶子节点,则删除该子树.

代码实现以上功能点:

public class BinaryTree {
    public static void main(String[] args) {
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        Node node4 = new Node(4);
        Node node5 = new Node(5);
        Node node6 = new Node(6);
        Node node7 = new Node(7);
        node1.left = node2;
        node1.right = node3;
        node2.left = node4;
        node2.right = node5;
        node3.left = node6;
        node3.right = node7;
        BinaryTreeDemo binaryTreeDemo = new BinaryTreeDemo();
        binaryTreeDemo.setRoot(node1);
        binaryTreeDemo.preOrder();
        System.out.println("====================");
        binaryTreeDemo.infixOrder();
        System.out.println("====================");
        binaryTreeDemo.postOrder();
        System.out.println("***********************");
        System.out.println(binaryTreeDemo.preSearch(6));

        System.out.println("-------------------------------");
        binaryTreeDemo.del(2);
        binaryTreeDemo.preOrder();
    }
}
class BinaryTreeDemo{
    Node root;

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

    public void preOrder(){
        if (root == null){
            throw new RuntimeException("空树");
        }else {
            root.preOrder();
        }
    }
    public void infixOrder(){
        if (root == null){
            throw new RuntimeException("空树");
        }else {
            root.infixOrder();
        }
    }
    public void postOrder(){
        if (root == null){
            throw new RuntimeException("空树");
        }else {
            root.postOrder();
        }
    }
    public Node preSearch(int no){
        if (root == null){
            System.out.println("空树");
            return null;
        }else {
            return root.preSearch(no);
        }
    }
    public void del(int no){
        if (root != null) {
            if (root.no == no) {
                root = null;
            } else {
                root.del(no);
            }
        }else {
            System.out.println("空树,不能删除~");
        }
    }
}
class Node{
    int no;
    Node left;
    Node right;

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

    @Override
    public String toString() {
        return "Node{" +
                "no=" + 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 Node preSearch(int no){
        System.out.println("进入前序遍历");
        Node temp = null;
        if (this.no == no){
            return this;
        }
        if (this.left != null){
            temp = this.left.preSearch(no);
        }
        if (temp != null){
            return temp;
        }
        if (this.right != null){
            temp = this.right.preSearch(no);
        }
            return temp;
    }
    public void del(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.del(no);
        }
        if (this.right != null){
            this.right.del(no);
        }
    }
}

6.2 顺序存储二叉树

从数据存储来看,数组存储方式树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,看示意图。

要求:

1) 右图的二叉树的结点,要求以数组 的方式来存放 arr : [1, 2, 3, 4, 5, 6, 6]
2) 要求在遍历数组 arr 时,仍然可以以 前序遍历 中序遍历 后序遍历 方式完成结点的遍历

顺序存储二叉树的特点:

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

完全二叉树:如果该二叉树的所有叶子结点都在最后一层或者倒数第二层,且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续

代码实现:

public class SeqBinaryTree {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6,7};
        int[] arr1 = {};
        SeqBinaryTreeDemo seqBinaryTreeDemo = new SeqBinaryTreeDemo(arr);
        seqBinaryTreeDemo.preOrder();
    }
}
class SeqBinaryTreeDemo{
    int[] arr;

    public SeqBinaryTreeDemo(int[] arr) {
        this.arr = arr;
    }
    public void preOrder(){
        preOrder(0);
    }
    public void preOrder(int index){
        if (arr == null || arr.length == 0){
            System.out.println("数组为空,无法遍历");
            return;
        }
        System.out.println(arr[index]);
        if ((2 * index + 1) < arr.length){
            preOrder(2 * index + 1);
        }
        if ((2 * index + 2) < arr.length){
            preOrder(2 * index + 2);
        }
    }
}

应用实例:堆排序(见6.4节)

6.3 线索化二叉树

将数列 {1, 3, 6, 8, 10, 14  } 构建成一颗二叉树.  n+1=7

问题分析:

1) 当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }
2) 但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上 .
3) 如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点 , 怎么办 ?
4) 解决方案 - 线索二叉树

线索二叉树基本介绍

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

线索二叉树应用案例

应用案例说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}

说明: 当线索化二叉树后,Node节点的 属性 left right ,有如下情况:

1) left 指向的是左子树,也可能是指向的前驱节点 . 比如 ① 节点 left 指向的左子树 , 而 ⑩ 节点的 left 指向的就是前驱节点 .
2) right 指向的是右子树,也可能是指向后继节点,比如 ① 节点 right 指向的是右子树,而⑩ 节点的 right 指向的是后继节点 .

遍历线索化二叉树

说明:对前面的中序线索化的二叉树, 进行遍历

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

代码实现:

public class ThreadedBinaryTreeDemo {
    public static void main(String[] args) {
        Node2 node1 = new Node2(1);
        Node2 node2 = new Node2(3);
        Node2 node3 = new Node2(6);
        Node2 node4 = new Node2(8);
        Node2 node5 = new Node2(10);
        Node2 node6 = new Node2(14);
        node1.left = node2;
        node1.right = node3;
        node2.left = node4;
        node2.right = node5;
        node3.left = node6;
        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree(node1);
        threadedBinaryTree.threadedNodes();
        threadedBinaryTree.threadedList();
    }
}
class ThreadedBinaryTree{
    Node2 root;

    public ThreadedBinaryTree(Node2 root) {
        this.root = root;
    }

    Node2 pre = null;
    public void threadedNodes(){
        this.threadedNodes(root);
    }
    public void threadedNodes(Node2 node){
        if (node == null){
            return;
        }
        threadedNodes(node.left);
        if (node.left == null){
            node.left = pre;
            node.leftType = 1;
        }
        if (pre != null && pre.right == null){
            pre.right = node;
            pre.rightType = 1;
        }
        pre = node;
        threadedNodes(node.right);
    }
    public void threadedList(){
        Node2 node = root;
        while (node != null){
            while (node.leftType == 0){
                node = node.left;
            }
            System.out.println(node);
            if (node.rightType == 1){
                node = node.right;
                System.out.println(node);
            }
            node = node.right;
        }
    }
}
class Node2{
    int no;
    Node2 left;
    Node2 right;
    int leftType;
    int rightType;

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

    @Override
    public String toString() {
        return "Node2{" +
                "no=" + no +
                '}';
    }
}

6.4 树结构的实际应用

(1)堆排序

堆排序基本介绍

1) 堆排序是利用 这种数据结构而设计的一种排序算法,堆排序是一种 选择排序, 它的最坏,最好,平均时间复杂度均为 O( nlogn ) ,它也是不稳定排序。
2) 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆 , 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
3) 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
4) 大顶堆举例说明
我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子 :

大顶堆特点arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]  // i 对应第几个节点,i0开始编号

5) 小顶堆举例说明

小顶堆:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2] // i 对应第几个节点,i0开始编号

6) 一般 升序采用大顶堆 降序采用小顶堆 

堆排序基本思想

堆排序的基本思想是:

1) 将待排序序列构造成一个大顶堆
2) 此时,整个序列的最大值就是堆顶的根节点。
3) 将其与末尾元素进行交换,此时末尾就为最大值。
4) 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.

堆排序步骤图解说明

要求:给一个数组{4,6,8,5,9},要求使用堆排序法,将数组升序排序

代码实现:

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = {4,6,8,5,9};
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void heapSort(int[] arr){
        int temp = 0;
        System.out.println("堆排序");
        for (int i = arr.length / 2 - 1; i >= 0; i--) {
            adjust(arr, i, arr.length);
        }
        for (int i = arr.length - 1; i > 0; i--) {
            temp = arr[i];
            arr[i] = arr[0];
            arr[0] = temp;
            adjust(arr, 0, i);
        }
    }
    public static void adjust(int[] arr, int i, int len){
        int temp = arr[i];
        for (int j = 2 * i + 1; j < len; j = 2 * j + 1) {
            if (j + 1 < len && arr[j] < arr[j + 1]){
                j++;
            }
            if (arr[j] > temp){
                arr[i] = arr[j];
                i = j;
            }else {
                break;
            }
        }
        arr[i] = temp;
    }
}

(2)赫夫曼树

基本介绍

1) 给定 n 个权值作为 n 叶子结点 ,构造一棵二叉树,若该树的 带权路径长度 ( wpl ) 达到最小,称这样的二叉树为 最优二叉树 ,也称为 哈夫曼树 (Huffman Tree) , 还有的书翻译为 霍夫曼树
2) 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

赫夫曼树几个重要概念和举例说明

1) 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1 ,则从根结点到第 L 层结点的路径长度为 L-1
2) 结点的权及带权路径长度: 若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。 结点的带权路径长度 为:从根结点到该结点之间的路径长度与该结点的权的乘积
3) 树的带权路径长度: 树的带权路径长度规定为所有 叶子结点 的带权路径长度之和,记为 WPL(weighted path length) , 权值越大的结点离根结点越近的二叉树才是最优二叉树。
4) WPL 最小的就是赫夫曼树

赫夫曼树创建思路图解

给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.

思路分析(示意图)

构成赫夫曼树的步骤:

1) 从小到大进行排序 , 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
2) 取出根节点权值最小的两颗二叉树
3) 组成一颗新的二叉树 , 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和 
4) 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复  1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
5) 图解:
代码实现:
public class HuffmanTreeDemo {
    public static void main(String[] args) {
        int[] arr = {13,7,8,3,29,6,1};
//        createHuffman(arr);
        preOrder(createHuffman(arr));
    }
    public static void preOrder(Node root){

        if (root != null){
            root.preOrder();
        }else {
            System.out.println("空树。");
            return;
        }
    }
    public static Node createHuffman(int[] arr){
        ArrayList<Node> nodes = new ArrayList<>();
        for (int i = 0; i < arr.length; i++) {
            nodes.add(new Node(arr[i]));
        }
//        Collections.sort(nodes);
        while (nodes.size() > 1){
            Collections.sort(nodes);
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            Node newNode = new Node(leftNode.value + rightNode.value);
            newNode.left = leftNode;
            newNode.right = rightNode;
            nodes.add(newNode);
            nodes.remove(leftNode);
            nodes.remove(rightNode);
        }
        return nodes.get(0);
    }
}
class Node implements Comparable<Node>{
    int value;
    Node left;
    Node right;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    @Override
    public int compareTo(Node o) {
        return this.value - o.value;
    }

    public void preOrder(){
        System.out.println(this);
        if (this.left != null){
            this.left.preOrder();
        }
        if (this.right != null){
            this.right.preOrder();
        }
    }
}

赫夫曼编码:

基本介绍

1) 赫夫曼编码也翻译为     哈夫曼 编码 (Huffman Coding) ,又称霍夫曼编码,是一种编码方式 , 属于一种程序算法
2) 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
3) 赫夫曼编码广泛地用于数据文件压缩。其 压缩率通常在 20% 90% 之间
4) 赫夫曼码是可变 字长 编码 (VLC) 的一种。 Huffman 1952 年提出一种编码方法,称之为最佳编码

原理剖析

通信领域中信息的处理方式 1- 定长编码
i like like like java do you like a java       // 40 个字符 ( 包括空格
105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97  // 对应 Ascii
01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 // 对应的二进制
按照二进制来传递信息,总的长度是  359   ( 包括空格 )

在线转码 工具 :https://www.mokuge.com/tool/asciito16/

通信领域中信息的处理方式 2- 变长编码
i like like like java do you like a java       // 40 个字符 ( 包括空格 )
d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9  // 各个字符对应的个数
0=  ,  1=a, 10= i , 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d
 
说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了 9 次, 编码为 0 , 其它依次类推 .
按照上面给各个字符规定的编码,则我们在传输  " i like like like java do you like a java" 数据时,编码就是
10 0 101 10 100 ... 
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码

原理剖析

通信领域中信息的处理方式3-赫夫曼编码

1)i like like like java do you like a java       // 40 个字符 ( 包括空格 )
2)d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9  // 各个字符对应的个数
3)按照上面字符出现的次数构建一颗赫夫曼树 , 次数作为权值 .( 图后 )

4)根据赫夫曼树,给各个字符规定编码 , 向左的路径为0向右的路径为1 , 编码如下:

o: 1000   u: 10010  d: 100110  y: 100111    i: 101          a : 110     k: 1110    e: 1111       j: 0000       v: 0001         l: 001          : 01

5)按照上面的赫夫曼编码,我们的"i like like like java do you like a java"   字符串对应的编码为 (注意这里我们使用的无损压缩)

1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110

通过赫夫曼编码处理,长度为 : 133

说明:

1) 原来长度是  359 , 压缩了  (359-133) / 359 = 62.9%
2) 此编码满足前缀编码 , 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性

赫夫曼编码是无损处理方案。

注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:

最佳实践-数据压缩(创建赫夫曼树)

将给出的一段文本,比如 "i like like like java do you like a java" , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理 ,形式"1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110"

步骤1:根据赫夫曼编码压缩数据的原理,需要创建 "i like like like java do you like a java" 对应的赫夫曼树.

最佳实践-数据压缩(生成赫夫曼编码和赫夫曼编码后的数据)

已经生成了 赫夫曼树, 下面继续完成任务

1) 生成赫夫曼树对应的赫夫曼编码  , 如下表 :
=01 a=100 d=11000 u=11001 e=1110 v=11011 i =101 y=11010 j=0010 k=1111 l=000 o=0011
2) 使用赫夫曼编码来生成赫夫曼编码数据 , 按照上面的赫夫曼编码,将 " i like like like java do you like a java "   字符串生成对应的编码数据 , 形式如下 .
1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

最佳实践-数据解压(使用赫夫曼编码解码)
使用赫夫曼编码来解码数据,具体要求是

1) 前面我们得到了赫夫曼编码和对应的编码 byte[] , :[-88, -65, -56, -65, -56, -65, -55, 77
, -57, 6, -24, -14, -117, -4, -60, -90, 28]
2) 现在要求使用赫夫曼编码, 进行解码,又 重新得到原来的字符串 " i like like like  java do you like a java "

思路:解码过程,就是编码的一个逆向操作。

最佳实践-文件压缩
通过赫夫曼编码对一个字符串进行编码和解码, 下面完成对文件的压缩和解压, 具体要求:给你一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。

思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩

最佳实践-文件解压(文件恢复)
具体要求:将前面压缩的文件,重新恢复成原来的文件。

思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)

赫夫曼编码压缩文件注意事项

1) 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化 , 比如视频 ,ppt 等等文件  [ 举例压一个 .ppt ]
2) 赫夫曼编码是按字节来处理的,因此可以处理所有的文件 ( 二进制文件、文本文件 ) [ 举例压一个 .xml 文件 ]
3) 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显 .

代码实现:

public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        List<Node1> nodes = getNodes(content);
        Node1 root = createHuffmanTree(nodes);
        preOrder(root);
        Map<Byte, String> huffmanCode = getHuffmanCode(root);
        System.out.println(huffmanCode);
        String codes = getCodes(huffmanCode, content);
        System.out.println(codes);
        byte[] huffmanByte = zip(codes);
        System.out.println(Arrays.toString(huffmanByte));
        byte[] bytes = deCode(huffmanByte, huffmanCode);
        System.out.println(Arrays.toString(bytes));
        System.out.println(new String(bytes));
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            sb.append((char) bytes[i]);
        }
        System.out.println(sb.toString());
    }
    public static byte[] deCode(byte[] bytes, Map<Byte, String> huffmanCode){
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            byte b = bytes[i];
            boolean flag = (i == bytes.length - 1);
            stringBuilder.append(byteToBitString(b, !flag));
        }
        HashMap<String, Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> entry : huffmanCode.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        ArrayList<Byte> list = new ArrayList<>();
        for (int i = 0; i < stringBuilder.length();) {
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag){
                String substring = stringBuilder.substring(i, i + count);
                b = map.get(substring);
                if (b == null){
                    count++;
                }else {

                    flag = false;
                }
            }
            list.add(b);
            i = i + count;

        }
        byte[] bytes1 = new byte[list.size()];
        int i = 0;
        for (int j = 0; j < list.size(); j++) {
            bytes1[i] = list.get(j);
            i++;
        }
        return bytes1;
    }
    public static String byteToBitString(byte b, boolean flag){               //
        int temp = b;
        if (flag){
            temp |= 256;
        }
        String str = Integer.toBinaryString(temp);
        if (flag){
            return str.substring(str.length() - 8);
        }else {
            return str;
        }
    }

//    封装一个整体的方法

    public static byte[] zip(String huffmanCode){
        int len = (huffmanCode.length() + 7) / 8;
        byte[] bytes = new byte[len];
        String substr = "";
        int count = 0;
        for (int i = 0; i < huffmanCode.length(); i += 8) {
            substr = "";
            if (i + 8 > huffmanCode.length()){
                substr = huffmanCode.substring(i);
            }else {
                substr = huffmanCode.substring(i, i + 8);
            }
            bytes[count++] = (byte)Integer.parseInt(substr,2);
        }
        return bytes;
    }
    public static String getCodes(Map<Byte, String> huffmanCode, String str){
        StringBuilder stringBuilder = new StringBuilder();
        byte[] bytes = str.getBytes();
        for (byte aByte : bytes) {
            stringBuilder.append(huffmanCode.get(aByte));
        }
        return stringBuilder.toString();
    }
    public static Map<Byte, String> huffmanCode = new HashMap<>();
    public static StringBuilder stringBuilder = new StringBuilder();
    public static Map<Byte, String> getHuffmanCode(Node1 node){
        if (node == null){
            return null;
        }
        getHuffmanCode(node.left, "0", stringBuilder);
        getHuffmanCode(node.right, "1", stringBuilder);
        return huffmanCode;
    }
    public static Map<Byte, String> getHuffmanCode(Node1 node, String code, StringBuilder stringBuilder){
        StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
        stringBuilder1.append(code);
        if (node != null){
            if (node.data == null){
                getHuffmanCode(node.left, "0", stringBuilder1);
                getHuffmanCode(node.right, "1", stringBuilder1);
            }else {
                huffmanCode.put(node.data, stringBuilder1.toString());
            }
        }
        return huffmanCode;
    }
    public static List<Node1> getNodes(String str){
        ArrayList<Byte> bytes = new ArrayList<>();
        for (int i = 0; i < str.length(); i++) {
            bytes.add((byte)str.charAt(i));
        }
        HashMap<Byte, Integer> map = new HashMap<>();
        for (Byte aByte : bytes) {
            if (map.get(aByte) == null){
                map.put(aByte, 1);
            }else {
                map.put(aByte, map.get(aByte) + 1);
            }
        }
        ArrayList<Node1> nodes = new ArrayList<>();
        for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
            nodes.add(new Node1(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }
    public static Node1 createHuffmanTree(List<Node1> nodes){
        while (nodes.size() > 1){
            Collections.sort(nodes);
            Node1 leftNode = nodes.get(0);
            Node1 rightNode = nodes.get(1);
            Node1 parent = new Node1(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parent);
        }
        return nodes.get(0);
    }
    public static void preOrder(Node1 root){
        if (root == null){
            return;
        }else {
            root.preOrder();
        }
    }
}
class Node1 implements Comparable<Node1>{
    Byte data;
    int weight;
    Node1 left;
    Node1 right;

    public Node1(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "Node1{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    public void preOrder(){
        System.out.println(this);
        if (this.left != null){
            this.left.preOrder();
        }
        if (this.right != null){
            this.right.preOrder();
        }
    }

    @Override
    public int compareTo(Node1 o) {
        return this.weight - o.weight;
    }
}

(3)二叉排序树(BST)

先看一个需求

给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加。

解决方案分析

使用数组
1) 数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢 . [ 示意图 ]
2) 数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。 [ 示意图 ]

使用链式存储 - 链表
不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。 [ 示意图 ]
使用二叉排序树

二叉排序树介绍

二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。

特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点

比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:

二叉排序树创建和遍历

一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为 :

二叉排序树的删除

二叉排序树的删除情况比较复杂,有下面三种情况需要考虑

1) 删除叶子节点 ( 比如: 2, 5, 9, 12)
2) 删除只有一颗子树的节点 ( 比如: 1)
3) 删除有两颗子树的节点 . ( 比如: 7, 3 10 )

删除叶子节点
删除的节点是叶子节点,即该节点下没有左右子节点。
比如这里的 ( 比如: 2, 5, 9, 12)

        删除节点有一个子节点
        删除的节点有一个子节点,即该节点有左子节点或者右子节点。比如这里的 (比如:1 )

删除节点有两个子节点
删除的节点有两个子节点,即该节点有左子节点和右子节点。比如这里的 ( 比如: 7,3,10)

删除叶子节点:

  1. 先找到要删除的节点:target
  2. 找到target的父节点parent
  3. 判断target是parent的左/右子节点
  4. 是左子结点(parent.left = null);是右子节点(parent.right = null)

删除只有一棵子树的节点:

  1. 先找到要删除的节点target及其父节点parent;
  2. 确定target的子节点是左/右子节点;
  3. 确定target是parent的左/右子节点;
  4. 当target有左子节点:
    • 若target是parent的左子结点:parent.left = target.left;
    • 若target是parent的右子节点:parent.right = target.left;
  5. 若target有右子节点:
    • 若target是parent的左子结点:parent.left = target.right;
    • 若target是parent的右子节点:parent.right = target.right;

删除有两棵子树的节点:

  1. 找到要删除的节点target及其父节点parent;
  2. 从target的右子树找到最小节点(或从左子树找到最大节点)
  3. 用临时变量temp保存找到的该最大(小)值,并删除该节点。
  4. Target.value = temp;

代码实现:与下一节平衡二叉树一起

(4)平衡二叉树(AVL树/二叉搜索树)

看一个案例(说明二叉排序树可能的问题)

给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.

左边BST 存在的问题分析:

1) 左子树全部为空,从形式上看,更像一个单链表 .
2) 插入速度没有影响
3) 查询速度明显降低 ( 因为需要依次比较 ), 不能发挥 BST 的优势,因为每次还需要比较左子树,其查询速度比 单链表还慢
4) 解决方案 - 平衡二叉树 (AVL)

基本介绍

1) 平衡二叉树也叫平衡二叉搜索树( Self-balancing binary search tree )又被称为 AVL 树, 可以 保证查询效率较高
2)具有以下特点 :它是一 棵空树或它的左右两个子树的高度差的绝对值不超过 1 ,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有 红黑树 AVL 替罪羊树 Treap 伸展树 等。

应用案例-单旋转(左旋转)

应用案例-单旋转(右旋转)

应用案例-双旋转

前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列

int[] arr = { 10, 11, 7, 6, 8, 9 };  运行原来的代码可以看到,并没有转成 AVL.

int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL

总结:

左旋:rightHeight() – leftHeight() > 1 时:

  1. 创建一个新节点,值为当前根节点的值;
  2. 新节点的左子树设为当前节点左子树;
  3. 新节点右子树设为当前节点右子树的左子树;
  4. 当前节点的值换为右子节点的值;
  5. 当前节点的右子树设置为右子树的右子树的值;
  6. 当前节点的左子树设为新节点;

右旋:与左旋相反

双旋:当前节点左子树的右子树高度>左子树高度;先对当前节点左子树进行左旋,再对当前节点右旋;

代码实现:

public class BinarySortTree {
    public static void main(String[] args) {
//        Nodes node1 = new Nodes(7);
//        Nodes node2 = new Nodes(3);
//        Nodes node3 = new Nodes(10);
//        Nodes node4 = new Nodes(12);
//        Nodes node5 = new Nodes(5);
//        Nodes node6 = new Nodes(1);
//        Nodes node7 = new Nodes(9);
//
//        BinarySortTrees binarySortTrees = new BinarySortTrees(node1);
//        binarySortTrees.add(node2);
//        binarySortTrees.add(node3);
//        binarySortTrees.add(node4);
//        binarySortTrees.add(node5);
//        binarySortTrees.add(node6);
//        binarySortTrees.add(node7);
//        binarySortTrees.infixOrder();

        Nodes node1 = new Nodes(10);
        Nodes node2 = new Nodes(11);
        Nodes node3 = new Nodes(7);
        Nodes node4 = new Nodes(6);
        Nodes node5 = new Nodes(8);
        Nodes node6 = new Nodes(9);
        BinarySortTrees binarySortTrees = new BinarySortTrees();
        binarySortTrees.add(node1);
        binarySortTrees.add(node2);
        binarySortTrees.add(node3);
        binarySortTrees.add(node4);
        binarySortTrees.add(node5);
        binarySortTrees.add(node6);
        System.out.println("++++++++++++++++++++++++++++++");
//        System.out.println(binarySortTrees.getTarget(1));
//        System.out.println(binarySortTrees.getTarget(5));
//        System.out.println(binarySortTrees.getTarget(9));
//        System.out.println("=================================");
//        binarySortTrees.getParent(3);
//        System.out.println(binarySortTrees.getParent(3));
//        System.out.println(binarySortTrees.getParent(5));
//        System.out.println(binarySortTrees.getParent(1));
//        System.out.println(binarySortTrees.getParent(7));
//        binarySortTrees.delNode(9);
        binarySortTrees.infixOrder();
        System.out.println(binarySortTrees.leftHeight(node1));
        System.out.println(binarySortTrees.rightHeight(node1));
    }
}
class BinarySortTrees{
    Nodes root;

    public BinarySortTrees() {
    }

    public BinarySortTrees(Nodes root) {
        this.root = root;
    }

    public int leftHeight(Nodes nodes){
        return nodes.leftHeight();
    }
    public int rightHeight(Nodes nodes){
        return nodes.rightHeight();
    }
    public void add(Nodes node){
        if (root == null){
            root = node;
        }else {
            root.addNode(node);
        }
    }
    public void infixOrder(){
        if (root != null){
            root.infixOrder();
        }else {
            return;
        }
    }
    public Nodes getTarget(int value){
        if (root != null){
            return root.target(value);
        }else {
            return null;
        }
    }
    public Nodes getParent(int value){
        if (root != null){
            return root.parent(value);
        }else {
            return null;
        }
    }
    public int minRight(Nodes node){
        Nodes target = node;
        while (target.left != null){
            target = target.left;
        }
        delNode(target.value);
        return target.value;
    }
    public int maxLeft(Nodes node){
        Nodes target = node;
        while (target.right != null){
            target = target.right;
        }
        delNode(target.value);
        return target.value;
    }
    public void delNode(int value){
        Nodes target = getTarget(value);
        if (target == null){
            System.out.println("不存在");
            return;
        }else {
            if (root.left == null && root.right == null){
                root = null;
            }
            Nodes parent = getParent(value);
            if (target.left == null && target.right == null){
                if (parent != null && parent.left.value == value){
                    parent.left = null;
                }else if (parent != null && parent.right.value == value){
                    parent.right = null;
                }
            }else if (target.left != null && target.right != null){
                int max = maxLeft(target.left);
//                int min = minRight(target.right);
                target.value = max;
            }else {
                if (target.left != null){
                    if (parent != null){
                        if (parent.left == target){
                            parent.left = target.left;
                        }else {
                            parent.right = target.left;
                        }
                    }else {
                        root = target.left;
                    }
                }else if (target.right != null){
                    if (parent != null){
                        if (parent.left != null){
                            parent.left = target.right;
                        }else {
                            parent.right = target.right;
                        }
                    }else {
                        root = target.right;
                    }
                }
            }
        }
    }
}
class Nodes{
    int value;
    Nodes left;
    Nodes right;

    public Nodes(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Nodes{" +
                "value=" + value +
                '}';
    }
    public void addNode(Nodes node){
        if (node == null){
            return;
        }
        if (node.value < this.value){
            if (this.left == null){
                this.left = node;
            }else {
                this.left.addNode(node);
            }
        }else {
            if (this.right == null){
                this.right = node;
            }else {
                this.right.addNode(node);
            }
        }
        if (this.leftHeight() - this.rightHeight() > 1){
            if (this.left != null && this.left.rightHeight() > this.left.leftHeight()){                 /
                this.left.leftRotate();
                this.rightRotate();
            }else {
                this.rightRotate();///
            }
            return;
        }
        if (this.rightHeight() - this.leftHeight()  > 1){
            if (this.right != null && this.right.leftHeight() > this.right.rightHeight()){        ///
                this.right.rightRotate();
                this.leftRotate();
            }else {
                this.leftRotate();/
            }
        }
    }
    public void leftRotate(){
        Nodes newNode = new Nodes(this.value);
        newNode.left = this.left;
        newNode.right = this.right.left;
        this.value = this.right.value;
        this.right = this.right.right;
        this.left = newNode;
    }
    public void rightRotate(){
        Nodes newNode = new Nodes(this.value);
        newNode.right = this.right;
        newNode.left = this.left.right;
        this.value = this.left.value;
        this.left = this.left.left;
        this.right = newNode;
    }
    public int leftHeight(){
        if (this.left == null){
            return 0;            //
        }
        return this.left.height();
    }
    public int rightHeight(){
        if (this.right == null){
            return 0;
        }
        return this.right.height();
    }
    public int height(){
        return Math.max(this.left == null?0: this.left.height(), this.right == null?0: this.right.height())+1;
    }
    public void infixOrder(){
        if (this.left != null){
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null){
            this.right.infixOrder();
        }
    }
    public Nodes target(int value){
        if (this.value == value){
            return this;
        }else if (this.left != null && value < this.value){
            return this.left.target(value);
        }else if (this.right != null && value > this.value){
            return this.right.target(value);
        }else {
            return null;
        }
    }
    public Nodes parent(int value){
        if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)){
            return this;
        }else if (this.left != null && value < this.value){
            return this.left.parent(value);
        }else if (this.right != null && value > this.value){
            return this.right.parent(value);
        }else {
            return null;
        }
    }
}

6.5 多路查找树

7. 图

为什么要有图

1) 前面我们学了 线性表和树
2) 线性表局限于一个直接前驱和一个直接后继的关系
3) 树也只能有一个直接前驱也就是父节点
4) 当我们需要表示多对多的关系时, 这里我们就用到了图

图的举例说明

图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。 结点也可以称为顶点。如图:

图的常用概念:

1) 顶点 (vertex)
2) (edge)
3) 路径
4) 无向图 ( 右图):顶点之间的连接没有方向,比如A-B, 即可以是 A-> B 也可以 B->A .

    路径比如从 D -> C 的路径有  1) D->B->C  2) D->A->B->C

5)  有向图:顶点之间的连接有方向,比如A-B, 只能是 A-> B 不能是 B->A .

        6) 带权图:这种边带权值的图也叫网.

           

图的表示方式:

图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)。
邻接矩阵

邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是的rowcol表示的是1....n个点。

邻接表

1)    邻接矩阵需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在 , 会造成空间的一定损失 .
2)邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+ 链表组成

说明 :
1) 标号为 0 的结点的相关联的结点为 1 2 3 4
2) 标号为 1 的结点的相关联结点为 0 4
3) 标号为 2 的结点相关联的结点为 0 4 5
4) ....
图的快速入门案例
1) 要求 : 代码实现如下图结构 .
2) 思路分析  (1) 存储顶点 String  使用 ArrayList (2) 保存矩阵 int[][] edges
3) 代码实现

图遍历介绍

所谓图的遍历,即是对结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略: (1)深度优先遍历 (2)广度优先遍历

7.1 图的深度优先遍历

深度优先遍历基本思想

图的深度优先搜索(Depth First Search)

1) 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解:每次都在访问完 当前结点 后首先访问 当前结点的第一个邻接结点
2) 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
3)显然,深度优先搜索是一个递归的过程

深度优先遍历算法步骤

1) 访问初始结点 v ,并标记结点 v 为已访问。
2) 查找结点 v 的第一个邻接结点 w
3) w 存在,则继续执行 4 ,如果 w 不存在,则回到第 1 步,将从 v 的下一个结点继续。
4) w 未被访问,对 w 进行深度优先遍历递归(即把 w 当做另一个 v ,然后进行步骤 123 )。
5) 查找结点 v w 邻接结点的下一个邻接结点,转到步骤 3

看一个具体案例分析:

7.2 图的广度优先遍历

广度优先遍历基本思想

图的广度优先搜索(Broad First Search)

类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点

广度优先遍历算法步骤

1) 访问初始结点 v 并标记结点 v 为已访问。
2) 结点 v 入队列
3) 当队列非空时,继续执行,否则算法结束。
4) 出队列,取得队头结点 u
5) 查找结点 u 的第一个邻接结点 w
6) 若结点 u 的邻接结点 w 不存在,则转到步骤 3 ;否则循环执行以下三个步骤:

                6.1 若结点w尚未被访问,则访问结点w并标记为已访问。

                6.2 结点w入队列

                6.3 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6

图的深度优先 VS 广度优先

graph.insertEdge(0, 1, 1);

graph.insertEdge(0, 2, 1);

graph.insertEdge(1, 3, 1);

graph.insertEdge(1, 4, 1);

graph.insertEdge(3, 7, 1);

graph.insertEdge(4, 7, 1);

graph.insertEdge(2, 5, 1);

graph.insertEdge(2, 6, 1);

graph.insertEdge(5, 6, 1);

1) 深度优先遍历顺序为  1->2->4->8->5->3->6->7
2) 广度优先算法的遍历顺序为: 1->2->3->4->5->6->7->8

代码实现:

public class Graph {
    ArrayList<String> vertexList;
    int[][] edges;
    boolean[] isVisited;
    public static void main(String[] args) {
        Graph graph = new Graph(8);
        String[] vertexes = {"1","2","3","4","5","6","7","8"};
        for (int i = 0; i < vertexes.length; i++) {
            graph.addVertex(vertexes[i]);
        }

        graph.createGraph(0,1,1);
        graph.createGraph(0,2,1);
        graph.createGraph(1,3,1);
        graph.createGraph(1,4,1);
        graph.createGraph(3,7,1);
        graph.createGraph(4,7,1);
        graph.createGraph(2,5,1);
        graph.createGraph(2,6,1);
        graph.createGraph(5,6,1);
        graph.show();
        System.out.println();
        graph.dfs();
        System.out.println();
        System.out.println();
        graph.bfs();
    }
    public Graph(int n){
        vertexList = new ArrayList<>(n);
        edges = new int[n][n];
    }

    public void dfs(boolean[] isVisited, int i){
        System.out.print(vertexList.get(i) + "->");
        isVisited[i] = true;
        int w = getFirstNeighbor(i);
        while (w != -1){
            while (!isVisited[w]){
                dfs(isVisited, w);
            }
            w = getNextNeighbor(i, w);
        }
    }
    public void dfs(){
        isVisited = new boolean[vertexList.size()];
        for (int i = 0; i < vertexList.size(); i++) {
            if (!isVisited[i]){
                dfs(isVisited, i);
            }
        }
    }

    public void bfs(boolean[] isVisited, int i){
        LinkedList queue = new LinkedList();
        System.out.print(vertexList.get(i) + "->");
        isVisited[i]= true;
        int u;
        int w;
        queue.add(i);
        while (!queue.isEmpty()){
            u = (Integer)queue.removeFirst();
            w = getFirstNeighbor(u);
            while (w != -1){
                if (!isVisited[w]){
                    System.out.print(vertexList.get(w) + "->");
                    isVisited[w] = true;
                    queue.add(w);
                }
                w = getNextNeighbor(u, w);
            }
        }
    }
    public void bfs(){
        isVisited = new boolean[vertexList.size()];
        for (int i = 0; i < vertexList.size(); i++) {
            if (!isVisited[i]){
                bfs(isVisited, i);
            }
        }
    }

    public void createGraph(int row, int col, int weight){
        edges[row][col] = weight;
        edges[col][row] = weight;
    }

    public void addVertex(String vertex){
        vertexList.add(vertex);
    }
    public void show(){
        for (int i = 0; i < edges.length; i++) {
            System.out.println(Arrays.toString(edges[i]));
        }
    }
    public int getFirstNeighbor(int row){
        for (int i = 0; i < vertexList.size(); i++) {
            if (edges[row][i] > 0) {
                return i;
            }
        }
        return -1;
    }
    public int getNextNeighbor(int row, int col){
        for (int i = col + 1; i < vertexList.size(); i++) {
            if (edges[row][i] > 0){
                return i;
            }
        }
        return -1;
    }
}

二、算法

1. 递归

实际应用场景:迷宫问题(回溯)、递归(Recursion)

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

递归调用机制:

(1)打印问题

(2)阶乘问题

递归可以解决的问题:

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

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

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

1.1 迷宫问题

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

代码实现:

public class Migong {
    public static void main(String[] args) {
        int[][] migong = new int[8][7];
        for (int i = 0; i < migong.length; i++) {
            migong[i][0] = 1;
            migong[i][6] = 0;
        }
        for (int i = 0; i < migong[0].length; i++) {
            migong[0][i] = 1;
            migong[7][i] = 1;
        }
        migong[2][1] = 1;
        migong[2][2] = 1;
        System.out.println("原始迷宫为:");
        for (int[] rows : migong) {
            for (int cols : rows) {
                System.out.print(cols + "\t");
            }
            System.out.println();
        }
        System.out.println("走过的路线为:");
        isWay(1,1, migong);
        for (int[] rows : migong) {
            for (int cols : rows) {
                System.out.print(cols + "\t");
            }
            System.out.println();
        }
    }
    public static boolean isWay(int x, int y, int[][] arr){
        if (arr[6][5] == 2){
            return true;
        }else {
            if (arr[x][y] == 0){
                arr[x][y] = 2;
                if (isWay(x +1, y, arr)){
                    return true;
                }else if (isWay(x, y + 1, arr)){
                    return true;
                }else if (isWay(x - 1, y, arr)){
                    return true;
                }else if (isWay(x, y - 1, arr)){
                    return true;
                }else {
                    arr[x][y] = 3;
                    return false;
                }
            }else {
                return false;
            }
        }
    }
}

1.2 八皇后问题

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

八皇后问题算法思路分析

1) 第一个皇后先放第一行第一列
2) 第二个皇后放在第二行第一列、然后判断是否 OK , 如果不 OK ,继续放在第二列、第三列、依次把所有列都放完,找到一个合适
3) 继续第三个皇后,还是第一列、第二列 …… 直到第 8 个皇后也能放在一个不冲突的位置,算是找到了一个正确解
4) 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到 .
5) 然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4 的步骤 

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

代码实现:

public class Queen8 {
    static int[] queen;
    static int count;
    static int totalCount;
    public static void main(String[] args) {
        queen = new int[8];
        count = 0;
        totalCount = 0;
        Queen8 queen8 = new Queen8();
        queen8.check(0);
        System.out.println("共有" + count + "种方法");
        System.out.println("共遍历了" + totalCount + "次");

    }

    public Queen8() {

    }

    public void check(int n){
        if (n == queen.length){
            print();
            return;
        }else {
            for (int i = 0; i < queen.length; i++) {
                queen[n] = i;
                if (judge(n)){
                    check(n+1);
                }
            }
        }
    }
    public boolean judge(int n){
        totalCount++;
        for (int i = 0; i < n; i++) {
            if (queen[n] == queen[i] || Math.abs(n - i) == Math.abs(queen[n] - queen[i])){
                return false;
            }
        }
        return true;
    }
    public void print(){
        count++;
        System.out.println(Arrays.toString(queen));
    }
}

2. 排序算法

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

排序的分类:

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

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

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

算法的时间复杂度:

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

1) 事后统计的方法
这种方法可行 , 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素 , 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快
2) 事前估算的方法
通过分析某个算法的 时间复杂度 来判断哪个算法更优 .

时间频度

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

举例:

忽略常数项

T(n)=2n+20

T(n)=2*n

T(3n+10)

T(3n)

1

22

2

13

3

2

24

4

16

6

5

30

10

25

15

8

36

16

34

24

15

50

30

55

45

30

80

60

100

90

100

220

200

310

300

300

620

600

910

900

忽略低次项:

T(n)=2n^2+3n+10

T(2n^2)

T(n^2+5n+20)

T(n^2)

1

15

2

26

1

2

24

8

34

4

5

75

50

70

25

8

162

128

124

64

15

505

450

320

225

30

1900

1800

1070

900

100

20310

20000

10520

10000

忽略系数:

T(3n^2+2n)

T(5n^2+7n)

T(n^3+5n)

T(6n^3+4n)

1

5

12

6

10

2

16

34

18

56

5

85

160

150

770

8

208

376

552

3104

15

705

1230

3450

20310

30

2760

4710

27150

162120

100

30200

50700

1000500

6000400

时间复杂度总结:

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

常见的时间复杂度

1) 常数阶 O(1) :直接输出
2) 对数阶 O( log n )(都是2为底):while循环
3) 线性阶 O(n):一层for
4) 线性对数阶 O(n log n ):for + while
5) 平方阶 O(n^2):两层for
6) 立方阶 O(n^3):三层for
7) k 次方阶 O( n^k )
8) 指数阶 O(2^n)

说明

常见的算法时间复杂度由小到大依次为: Ο(1) Ο( log n ) Ο( n) Ο( nlog n ) Ο( n^ 2 ) Ο( n^ 3 ) Ο( n^ k ) Ο( 2^ n ) ,随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低,对数均以2为底
从图中可见,我们应该尽可能避免使用指数阶的算法

1)常数阶O(1)

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

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

2)对数阶O(logn(2为底)

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

3) 线性阶O(n)

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

4)线性对数阶O(nlogN)

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

5)平方阶O(n²)

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

6) 立方阶O(n³)K次方阶O(n^k)

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

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

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

算法的空间复杂度:

基本介绍

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

2.1 冒泡排序

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

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

代码实现:

public class BubbleSort {
    public static void main(String[] args) {
        int count = 0;
        int[] alist = {12, -1, 4, 19, 23, 36};
        int temp = 0;
        boolean flag = false;
        for (int i = 0; i < alist.length - 1; i++) {
            for (int j = 0; j < alist.length - 1 - i; j++) {
                if (alist[j] < alist[j+1]){
                    flag = true;
                    temp = alist[j];
                    alist[j] = alist[j+1];
                    alist[j+1] = temp;
                }
                count++;
            }
            if (!flag){
                break;
            }else {
                flag  = false;
            }
        }
        System.out.println(count);
        for (int i = 0; i < alist.length; i++) {
            System.out.print(alist[i] + "\t");
        }
    }
}

2.2 选择排序

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

选择排序(select sorting)也是一种简单的排序方法。

它的基本思想是:

第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换;

第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换;

第三次从arr[2]~arr[n-1]中选取最小值,与arr[2]交换;

…;

i次从arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换;

…;

n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换;

总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

    public static void SelectionSort(int[] arr){
        for (int i = 0; i < arr.length - 1; i++) {
            int index = i;
            int max = arr[i];
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] > max){
                    max = arr[j];
                    index = j;
                }
            }
            if (index != i) {
                arr[index] = arr[i];
                arr[i] = max;
            }
        }
    }

2.3 插入排序

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

插入排序(Insertion Sorting)的基本思想是:

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

代码实现:

    public static void Insert(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int insertValue = arr[i];
            int insertIndex = i - 1;
            while (insertIndex >= 0 && insertValue < arr[insertIndex]){
                arr[insertIndex + 1] = arr[insertIndex];
                insertIndex -= 1;
            }
            arr[insertIndex +1] = insertValue;
        }
    }

2.4 希尔排序

简单插入排序存在的问题

我们看简单的插入排序可能存在的问题.

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

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

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

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

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

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

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

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

希尔排序法介绍

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

希尔排序法基本思想

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

代码实现:

//    交换法
    public static void changeShell(int[] arr){
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                for (int j = i - gap; j >= 0; j -= gap) {
                    if (arr[j] > arr[j + gap]){
                        int temp = arr[j];
                        arr[j] = arr[j+gap];
                        arr[j+gap] = temp;
                    }
                }
            }
        }
    }
//    插入法
    public static void insertShell(int[] arr){
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                int index = i;
                int insertValue = arr[i];
                while (index - gap >= 0 && insertValue < arr[index - gap]){
                    arr[index] = arr[index - gap];
                    index -= gap;
                }
                arr[index] = insertValue;
            }
        }
    }

2.5 快速排序

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

代码实现:

    public static void  quickSort(int[] arr, int left, int right){
        int l = left;
        int r = right;
        int pivot = arr[(left + right) / 2];
        while (l < r){
            while (arr[l] < pivot){
                l++;
            }
            while (arr[r] > pivot){
                r--;
            }
            if (l >= r){
                break;
            }
            int temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;
            if (arr[l] == pivot){
                r--;
            }
            if (arr[r] == pivot){
                l++;
            }
        }
        if (l == r){
            l++;
            r--;
        }
        if (left < r){
            quickSort(arr, left, r);
        }
        if (l < right){
            quickSort(arr, l, right);
        }
    }

2.6 归并排序

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

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。阶段可以理解为就是递归拆分子序列的过程。

合并相邻有序子序列:

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

归并排序:

    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);
        }
    }
    public static void merge(int[] arr, int left, int mid, int right, int[] temp){
        int l = left;
        int r = mid + 1;
        int t = 0;
        while (l <= mid && r <= right){
            if (arr[l] < arr[r]){
                temp[t] = arr[l];
                l++;
                t++;
            }else {
                temp[t] = arr[r];
                r++;
                t++;
            }
        }
        while (l <= mid){
            temp[t] = arr[l];
            t++;
            l++;
        }
        while (r <= right){
            temp[t] = arr[r];
            t++;
            r++;
        }
        t = 0;
        int leftTemp = left;
        while (leftTemp <= right){
            arr[leftTemp] = temp[t];
            leftTemp++;
            t++;
        }
    }

2.7 基数排序(桶排序)

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

基数排序基本思想:

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

基数排序图文说明

将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序。

代码实现:

    public static void radixSort(int[] arr){
        int[][] tong = new int[10][arr.length];
        int[] tongN = new int[10];
        int max = arr[0];
        for (int i = 0; i < arr.length; i++) {
            if (max < arr[i]){
                max = arr[i];
            }
        }
        int len = (max + "").length();
        for (int i = 0, n = 1; i < len; i++, n *= 10) {
            for (int j = 0; j < arr.length; j++) {
                int count = arr[j] / n % 10;
                tong[count][tongN[count]] = arr[j];
                tongN[count]++;
            }
            int index = 0;
            for (int j = 0; j < tongN.length; j++) {
                if (tongN[j] != 0){
                    for (int k = 0; k < tongN[j]; k++) {
                        arr[index++] = tong[j][k];
                    }
                }
                tongN[j] = 0;
            }
        }
    }
    public static void radix2(int[] arr){
        int max = arr[0];
        for (int i = 0; i < arr.length; i++) {
            if (max < arr[i]){
                max = arr[i];
            }
        }
        int len = (max + "").length();
        int[][] tong = new int[10][arr.length];
        int[] tongNum = new int[10];
        for (int i = 0, n = 1; i < len; i++, n *= 10) {
            for (int j = 0; j < arr.length; j++) {
                int m = arr[j] / n % 10;
                tong[m][tongNum[m]] = arr[j];
                tongNum[m]++;
            }
            int index = 0;
            for (int j = 0; j < tongNum.length; j++) {
                if (tongNum[j] != 0){
                    for (int k = 0; k < tongNum[j]; k++) {
                        arr[index++] = tong[j][k];
                    }
                }
                tongNum[j] = 0;
            }
        }
    }

基数排序的说明:

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

常用排序算法总结和对比

相关术语解释: 

1) 稳定 :如果 a 原本在 b 前面,而 a=b ,排序之后 a 仍然在 b 的前面;
2) 不稳定 :如果 a 原本在 b 的前面,而 a=b ,排序之后 a 可能会出现在 b 的后面;
3) 内排序 :所有排序操作都在内存中完成;
4) 外排序 :由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
5) 时间复杂度:  一个算法执行所耗费的时间。
6) 空间复杂度 :运行完一个程序所需内存的大小。
7) n: 数据规模
8) k: 桶”的个数
9) In-place:    不占用额外内存
10) Out-place: 占用额外内存

3. 查找算法

时间复杂度:

线性查找:3 * n+ 3;

二分查找:(floor(logn) +1) * 5 + 4

3.1 顺序查找

判断数列中是否包含此名称顺序查找 要求: 如果找到了,就提示找到,并给出下标值。

3.2 二分查找

二分查找的时间复杂度:O(logn);空间复杂度:O(1),因为只需要常数个指针 i,j,m

代码实现:

    public static int binarySearch(int[] arr, int left, int right, int value){
        int mid = (left + right) / 2;
        if (left <= right){
            if (value < arr[mid]){
                return binarySearch(arr, left, mid, value);
            }else if (value > arr[mid]){
                return binarySearch(arr, mid + 1, right, value);
            }else {
                return mid;
            }
        }else {
            return -1;
        }
    }
    public static List<Integer> binarySearch2(int[] arr, int left, int right, int value){
        int mid = (left + right) / 2;
        ArrayList<Integer> list = new ArrayList<>();
        if (left > right){
            return null;
        }
        if (value < arr[mid]){
            return binarySearch2(arr, left, mid, value);
        }else if (value > arr[mid]){
            return binarySearch2(arr, mid + 1, right, value);
        }else {
           int temp = mid - 1;
           while (temp >= 0 && arr[temp] == arr[mid]){
               list.add(temp);
               temp--;
           }
           list.add(mid);
           temp = mid + 1;
           while (temp < arr.length && arr[temp] == arr[mid]){
               list.add(temp);
               temp++;
           }
           return list;
        }
    }

3.3 插值查找

1)  插值查找算法类似于二分查找,不同的是插值查找每次从 自适应 mid 处开始查找。
2)  将折半查找中的求 mid 索引的公式 , low 表示左边索引 left, high 表示右边索引 right.
key
就是前面我们讲的  findVal

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

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

差值查找注意事项:

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

3.4 斐波那契查找

斐波那契(黄金分割法)查找基本介绍:

1)  黄金分割点是指把一条 线段 分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是 0.618 。由于按此比例设计的造型十分美丽,因此称为 黄金分割 ,也称为 中外比 。这是一个神奇的数字,会带来意向不大的效果。
2)  斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 发现斐波那契数列的两个相邻数 的比例,无限接近 黄金分割值 0.618

斐波那契(黄金分割法)原理:

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

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

1) 由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到  F[k]-1 = F[k-1]-1 + F[k-2]-1 +1  。该式说明:只要顺序表的长度为 F[k]-1 ,则可以将该表分成长度为 F[k-1]-1 F[k-2]-1 的两段,即如上图所示。从而中间位置为 mid= low+F (k-1)-1          
2) 类似的,每一子段也可以用相同的方式分割
3) 但顺序表长度 n 不一定刚好等于 F[k]-1 ,所以需要将原来的顺序表长度 n 增加至 F[k]-1 。这里的 k 值只要能使得 F[k]-1 恰好大于或等于 n 即可,由以下代码得到 , 顺序表长度增加后,新增的位置(从 n+1 F[k]-1 位置),都赋为 n 位置的值即可。

代码实现:

public class FibonacciSearch {
    public static int maxsize = 20;
    public static void main(String[] args) {
        int[] arr = {0,1,2,3,4,5,6,8,10,11,89,1000,1234};
        int index = fibonacciSearch(arr, 1000);
        System.out.println(index);
    }
    public static int[] fib(){
        int[] fib = new int[maxsize];
        fib[0] = 1;
        fib[1] = 1;
        for (int i = 2; i < maxsize; i++) {
            fib[i] = fib[i - 1] + fib[i - 2];
        }
        return fib;
    }
    public static int fibonacciSearch(int[] arr, int value){
        int left = 0;
        int right = arr.length - 1;
        int k = 0;    //斐波那契分割数值的下标
        int mid = 0;
        int f[] = fib();
        while (right > f[k] - 1){
            k++;
        }
        int[] temp = Arrays.copyOf(arr, f[k]);
        for (int i = right + 1; i < temp.length; i++) {
            temp[i] = arr[right];
        }
        while (left <= right){
            mid = left + f[k - 1] - 1;
            if (value < temp[mid]){
                right = mid - 1;
                k--;
            }else if (value > temp[mid]){
                left = mid + 1;
                k -= 2;
            }else {
                if (mid <= right){
                    return mid;
                }else {
                    return right;
                }
            }
        }
        return -1;
    }
}

4. 十种常用算法

4.1 二分查找(非递归形式)

1) 前面我们讲过了二分查找算法,是使用递归的方式,下面我们讲解二分查找算法的非递归方式
2) 二分查找法只适用于从有序的数列中进行查找 ( 比如数字和字母等 ) ,将数列排序后再进行查找
3) 二分查找法的运行时间为对数时间 O(㏒₂n) ,即查找到需要的目标位置 最多 只需要㏒₂ n 步,假设从 [0,99] 的队列 (100 个数,即 n=100) 中寻到目标数 30 ,则需要查找步数为㏒₂ 100 , 最多 需要查找 7 ( 2^6 < 100 < 2^7)

数组 {1,3, 8, 10, 11, 67, 100}, 编程实现二分查找, 要求使用非递归的方式完成.

    public static int binarySearch(int[] arr, int value){
        int left = 0;
        int right = arr.length - 1;
        while (left <= right){
            int mid = (left + right) / 2;
            if (value == arr[mid]){
                return mid;
            }else if (value < arr[mid]){
                right = mid - 1;
            }else {
                left = mid + 1;
            }
        }
        return -1;
    }

4.2 分治算法

1) 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题 …… 直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法 ( 快速排序 归并排序 ) ,傅立叶变换 ( 快速傅立叶变换 )……
2) 分治算法可以 求解的一些经典问题
二分搜索
大整数乘法
棋盘覆盖
线性时间选择
最接近点对问题
循环赛日程表
汉诺塔

分治算法的基本步骤

分治法在每一层递归上都有三个步骤:

1) 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
2) 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
3) 合并:将各个子问题的解合并为原问题的解。

分治(Divide-and-Conquer(P))算法设计模式如下

if |P|≤n0

   then return(ADHOC(P))

//P分解为较小的子问题 P1 ,P2 ,…,Pk

for i←1 to k

do yi ← Divide-and-Conquer(Pi)   递归解决Pi

T ← MERGE(y1,y2,…,yk)   合并子问题

return(T)

其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。

分治算法最佳实践-汉诺塔

汉诺塔的传说

汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

假如每秒钟一次,共需多长时间呢?移完这些金片需要5845.54亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了5845.54亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。

汉诺塔游戏的演示思路分析:   

1) 如果是有一个盘, A->C
2)如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1. 最下边的盘 2. 上面的盘
        1) 先把 最上面的盘 A->B
        2) 把最下边的盘 A->C
        3) B 塔的所有盘 从 B->C   

代码实现:

    public static void hanoiTower(int num, char a, char b, char c){
        if (num == 1){
            System.out.println("第" + num + "个飞盘:" + a + "->"+ c);
        }else {
            hanoiTower(num - 1, a, c, b);
            System.out.println("第" + num + "个飞盘:" + a + "->"+ c);
            hanoiTower(num - 1, b, a, c);
        }
    }

4.3 动态规划

动态规划算法介绍

1) 动态规划 ( Dynamic Programming ) 算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
2) 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
3) 与分治法不同的是, 适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
4) 动态规划可以通过 填表的方式 来逐步推进,得到最优解 .

应用场景-背包问题

背包问题:有一个背包,容量为4磅 , 现有如下物品

物品

重量

价格

吉他(G)

1

1500

音响(S)

4

3000

电脑(L)

3

2000

        1) 要求达到的目标为装入的背包的总价值最大,并且重量不超出

2)  要求装入的物品不能重复
3)  背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分 01 背包和完全背包 ( 完全背包指的是: 每种物品都有无限件可用 )
4)  这里的问题属于 01 背包,即每个物品最多放一个。而无限背包可以转化为 01 背包。
5)  算法的主要思想,利用动态规划来解决。每次遍历到的第 i 个物品,根据 w[ i ] v[ i ] 来确定是否需要将该物品放入背包中。即对于给定的 n 个物品,设 v[ i ] w[ i ] 分别为第 i 个物品的价值和重量, C 为背包的容量。再令 v[ i ][j] 表示在前 i 个物品中能够装入容量为 j 的背包中的最大价值。则我们有下面的结果:

(1)  v[ i ][0]=v[0][j]=0; // 表示 填入表 第一行和第一列是 0

      (2) w[i]> j 时:v[i][j]=v[i-1][j]   // 当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个单元格的装入策略

      (3) j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}  

// 当 准备加入的新增的商品的容量小于等于当前背包的容量,

// 装入的方式:

        v[i-1][j]: 就是上一个单元格的装入的最大值

        v[i] : 表示当前商品的价值

        v[i-1][j-w[i]] : 装入i-1商品,到剩余空间j-w[i]的最大值

        当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} :

代码实现:

public class Dynamic {
    public static void main(String[] args) {
        int[] w = {1,4,3};
        int[] v = {1500,3000,2000};
        int[][] table = new int[4][5];
        int[][] path = new int[4][5];
        for (int i = 0; i < table.length; i++) {
            table[i][0] = 0;
        }
        for (int i = 0; i < table[0].length; i++) {
            table[0][i] = 0;
        }
        for (int i = 0; i < table.length; i++) {
            System.out.println(Arrays.toString(table[i]));
        }
        System.out.println("+++++++++++++++++++++++++");
        for (int i = 1; i < table.length; i++) {
            for (int j = 1; j < table[0].length; j++) {
                if (w[i-1] > j){
                    table[i][j] = table[i - 1][j];
                }else {
                    if (table[i - 1][j] < v[i- 1] + table[i - 1][j - w[i - 1]]){
                        table[i][j] = v[i- 1] + table[i - 1][j - w[i - 1]];
                        path[i][j] = 1;
                    }else {
                        table[i][j] = table[i - 1][j];
                    }
                }
            }
        }
        for (int i = 0; i < table.length; i++) {
            System.out.println(Arrays.toString(table[i]));
        }
        System.out.println("=======================================");
        for (int i = 0; i < table.length; i++) {
            System.out.println(Arrays.toString(path[i]));
        }
        System.out.println("=======================================");
        int i = path.length - 1;
        int j = path[0].length - 1;
        while (i > 0 && j > 0){
            if (path[i][j] == 1){
                System.out.println("第" + i +"个物品被装进背包");
                System.out.println();
                j -= w[i - 1];
            }
            i--;
        }
    }
}

4.4 KMP算法

应用场景-字符串匹配问题

字符串匹配问题::

1) 有一个字符串 str1= "" 硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好 "" ,和一个子串 str2=" 尚硅谷你尚硅你 "
2) 现在要判断 str1 是否含有 str2 , 如果存在,就返回第一次出现的位置 , 如果没有,则返回 -1

KMP算法介绍

1) KMP 是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
2) Knuth-Morris-Pratt 字符串查找算法 ,简称为 “ KMP 算法”,常用于在一个文本串 S 内查找一个模式串 P 的出现位置,这个算法由 Donald Knuth Vaughan Pratt James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法 .
3) KMP 方法算法就利用之前判断过信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间

KMP算法最佳应用-字符串匹配问题

字符串匹配问题::

1) 有一个字符串 str1= "BBC ABCDAB ABCDABCDABDE" ,和一个子串 str2="ABCDABD"
2) 现在要判断 str1 是否含有 str2 , 如果存在,就返回第一次出现的位置 , 如果没有,则返回 -1
3) 要求:使用 KMP 算法完成判断,不能使用简单的暴力匹配算法 .
public class kmp {
    public static void main(String[] args) {
        String str2 = "ABCDABD";
        int[] next = getNext(str2);
        System.out.println(Arrays.toString(next));
        String str1 = "ABCDAB ABCDABCDABDE";    //BBC ABCDAB
        int search = search(str1, str2, next);
        System.out.println(search);
    }
    public static int search(String str1, String str2, int[] next){
        for (int i = 0, j = 0; i < str1.length(); i++) {
            while (j > 0 && str1.charAt(i) != str2.charAt(j)){
                j = next[j - 1];
            }
            if (str1.charAt(i) == str2.charAt(j)){
                j++;
            }
            if (j == str2.length()){
                return i - j + 1;
            }
        }
        return -1;
    }
    public static int[] getNext(String nextStr){
        int[] next = new int[nextStr.length()];
        next[0] = 0;
        for (int i = 1, j = 0; i < nextStr.length(); i++) {
            while (j > 0 && nextStr.charAt(i) != nextStr.charAt(j)){
                j = next[j - 1];
            }
            if (nextStr.charAt(i) == nextStr.charAt(j)){
                j++;
            }
            next[i] = j;
        }
        return next;
    }
}

4.5 贪心算法

贪心算法介绍

1) 贪婪算法 ( 贪心算法 ) 是指在对问题进行求解时,在每一步选择中都采取最好或者最优 ( 即最有利 ) 的选择,从而希望能够导致结果是最好或者最优的算法
2) 贪婪算法所得到的结果 不一定是最优的结果 ( 有时候会是最优解 ) ,但是都是相对近似 ( 接近 ) 最优解的结果

应用场景-集合覆盖问题

假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号

广播台

覆盖地区

K1

"北京", "上海", "天津"

K2

"广州", "北京", "深圳"

K3

"成都", "上海", "杭州"

K4

"上海", "天津"

K5

"杭州", "大连"

2) 思路分析 :
如何找出覆盖所有地区的广播台的集合呢,使用穷举法实现 , 列出每个可能的广播台的集合,这被称为幂集。假设总的有 n 个广播台,则广播台的组合总共有
2ⁿ -1 , 假设每秒可以计算 10 个子集, 如图 :

广播台数量n

子集总数2

需要的时间

5

32

3.2

10

1024

102.4

32

4294967296

13.6

100

1.26*100³º

4x10²³

使用贪婪算法,效率高 :

目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:

1) 遍历所有的广播电台 , 找到一个覆盖了最多 未覆盖的地区 的电台 ( 此电台可能包含一些已覆盖的地区,但没有关系)
2) 将这个电台加入到一个集合中 ( 比如 ArrayList ), 想办法把该电台覆盖的地区在下次比较时去掉。
3) 重复第 1 步直到覆盖了全部的地区

代码实现:

public class GreedyArithm {
    public static void main(String[] args) {
        HashMap<String, HashSet<String>> broadcast = new HashMap<>();
        HashSet<String> areas1 = new HashSet<>();
        areas1.add("北京");
        areas1.add("上海");
        areas1.add("天津");

        HashSet<String> areas2 = new HashSet<>();
        areas2.add("广州");
        areas2.add("北京");
        areas2.add("深圳");

        HashSet<String> areas3 = new HashSet<>();
        areas3.add("成都");
        areas3.add("上海");
        areas3.add("杭州");

        HashSet<String> areas4 = new HashSet<>();
        areas4.add("上海");
        areas4.add("天津");

        HashSet<String> areas5 = new HashSet<>();
        areas5.add("杭州");
        areas5.add("大连");

        broadcast.put("K1", areas1);
        broadcast.put("K2", areas2);
        broadcast.put("K3", areas3);
        broadcast.put("K4", areas4);
        broadcast.put("K5", areas5);

        ArrayList<String> allAreas = new ArrayList<>();
        allAreas.add("北京");
        allAreas.add("上海");
        allAreas.add("天津");
        allAreas.add("广州");
        allAreas.add("深圳");
        allAreas.add("成都");
        allAreas.add("杭州");
        allAreas.add("大连");

        String tempKey = null;
        HashSet<String> tempArea = new HashSet<>();
        ArrayList<String> selects = new ArrayList<>();
        while (!allAreas.isEmpty()){
            tempKey = null;
            for (String key : broadcast.keySet()) {
                tempArea.clear();
                HashSet<String> area = broadcast.get(key);
                tempArea.addAll(area);
                tempArea.retainAll(allAreas);
                if (tempArea.size() > 0 && (tempKey == null || tempArea.size() > broadcast.get(tempKey).size())){
                    tempKey = key;
                }

            }
            if (tempKey != null) {
                selects.add(tempKey);
                allAreas.removeAll(broadcast.get(tempKey));
            }
        }
        System.out.println(selects);
    }
}

贪心算法注意事项和细节

(1) 贪婪算法所得到的结果不一定是最优的结果 ( 有时候会是最优解 ) ,但是都是相对近似 ( 接近 ) 最优解的结果
(2) 比如上题的 算法选出的是 K1, K2, K3, K5 ,符合覆盖了全部的地区
(3) 但是我们发现 K2, K3,K4,K5 也可以覆盖全部地区,如果 K2 的使用成本低于 K1, 那么我们上题的 K1, K2, K3, K5 虽然是满足条件,但是并不是最优的 .

Prime:到不了的点间距离为10000,包括自身到自身;

Kruskal:到自身为0,到不了的距离为INF = Integer.MAX_VALUE;

Dijkstra:到不了的距离为N = 65535,包括自身到自身;

Floyd:到自身为0,到非直达的点距离为N;

4.6 Prim算法(最小生成树问题)

应用场景-修路问题

看一个应用场景和问题:


        1)有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通

2) 各个村庄的距离用边线表示 ( ) ,比如 A – B 距离 5 公里
3) 问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短 ?

思路: 10条边,连接即可,但是总的里程数不是最小.

正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少.

最小生成树

修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称MST给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树

1) N 个顶点,一定有 N-1条边
2)包 含全部顶点
3) N-1 条边都在图中
4) 举例说明 ( 如图 :)
5) 求最小生成树的算法主要是 普里姆 算法和克鲁斯卡尔算法

普里姆算法介绍

1) 普利姆 (Prim) 算法求最小生成树,也就是在包含 n 个顶点的连通图中,找出只有 (n-1) 条边包含所有 n 个顶点的连通子图,也就是所谓的 极小连通子图
2) 普利姆的算法如下 :
        (1) G=(V,E) 是连通网, T=(U,D) 是最小生成树, V,U 是顶点集合, E,D 是边的集合 
        (2) 若从顶点 u 开始构造最小生成树,则从集合 V 中取出顶点 u 放入集合 U 中,标记顶点 v visited[u]=1
        (3) 若集合 U 中顶点 ui 与集合 V-U 中的顶点 vj 之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点 vj 加入集合 U 中,将边( ui,vj )加入集合 D 中,标记 visited[ vj ]=1
        (4) 重复步骤②,直到 U V 相等,即所有顶点都被标记为访问过,此时 D 中有 n-1 条边

                (5)提示: 单独看步骤很难理解,我们通过代码来讲解,比较好理解.

普里姆算法最佳实践(修路问题)代码实现:

public class Prim {
//    char[] vertexes = {'A','B','C','D','E','F','G'};
//    int[][] matrix = {{}}
    public static void main(String[] args) {
        char[] vertexes = {'A','B','C','D','E','F','G'};
        int[][] matrix = {{10000,5,7,10000,10000,10000,2},
                {5,10000,10000,9,10000,10000,3},
                {7,10000,10000,10000,8,10000,10000},
                {10000,9,10000,10000,10000,4,10000},
                {10000,10000,8,10000,10000,5,4},
                {10000,10000,10000,4,5,10000,6},
                {2,3,10000,10000,4,6,10000}};
        boolean[] isVisit = new boolean[vertexes.length];
        isVisit[0] = true;
        int h1 = 0;
        int h2 = 0;
        for (int i = 1; i < vertexes.length; i++) {
            int maxWeight = 10000;
            for (int j = 0; j < vertexes.length; j++) {
                for (int k = 0; k < vertexes.length; k++) {
                    if (isVisit[j] == true && isVisit[k] == false && matrix[j][k] < maxWeight){
                        maxWeight = matrix[j][k];
                        h1 = j;
                        h2 = k;
                    }
                }
            }
            isVisit[h2] = true;
            System.out.println(vertexes[h1] + "->" + vertexes[h2] + " : " + matrix[h1][h2]);

        }
    }
}

4.7 Kruskal算法(公交站问题)

应用场景-公交站问题

看一个应用场景和问题:

1) 某城市新增 7 个站点 (A, B, C, D, E, F, G) ,现在需要修路把 7 个站点连通
2) 各个站点的距离用边线表示 ( ) ,比如 A – B 距离 12 公里
3) 问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短 ?

克鲁斯卡尔算法介绍

1) 克鲁斯卡尔 (Kruskal) 算法,是用来求加权连通图的最小生成树的算法
2) 基本思想 :按照权值从小到大的顺序选择 n-1 条边,并保证这 n-1 条边不构成回路
3) 具体做法 :首先构造一个只含 n 个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止

应用场景-公交站问题代码实现:

public class KruskalDemo {
    char[] data;
    int[][] matrix;
    int vertex;
//    static int nums = 0;
    public static final int INF = Integer.MAX_VALUE;
    public static void main(String[] args) {
        char[] data = {'A','B','C','D','E','F','G'};
        int vertex = data.length;

        int[][] matrix = {
                {0, 12, INF,INF,INF,16,14},
                {12,0,10,INF,INF,7,INF},
                {INF,10,0,3,5,6,INF},
                {INF,INF,3,0,4,INF,INF},
                {INF,INF,5,4,0,2,8},
                {16,7,6,INF,2,0,9},
                {14,INF,INF,INF,8,9,0}
        };
        KruskalDemo kruskalDemo = new KruskalDemo(data, matrix, vertex);
        kruskalDemo.krus();

//        KGraph[] edge = kruskalDemo.getEdge();
//        for (KGraph kGraph : edge) {
//            System.out.println(kGraph);
//        }
    }

    public KruskalDemo(char[] data, int[][] matrix, int vertex) {
        this.data = data;
        this.matrix = matrix;
        this.vertex = vertex;
    }

    public void krus(){
        KGraph[] edge = getEdge();
        sortEdges(edge);
        int index = 0;
        int nums = getNums();
        int[] ends = new int[nums];
        KGraph[] rets = new KGraph[nums];
        for (int i = 0; i < nums; i++) {
            int p1 = getIndex(edge[i].start);
            int p2 = getIndex(edge[i].end);
            int m = getEnds(ends, p1);
            int n = getEnds(ends, p2);
            if (m != n){
                rets[index++] = edge[i];
                ends[m] = n;
            }
        }
        for (int i = 0; i < index; i++) {
            System.out.println(rets[i ]);
        }
    }
    public int getIndex(char ch){
        for (int i = 0; i < vertex; i++) {
            if (data[i] == ch){
                return i;
            }
        }
        return -1;
    }
    public KGraph[] getEdge(){
        int nums = getNums();
        KGraph[] edges = new KGraph[nums];
        int index = 0;
        for (int i = 0; i < vertex; i++) {
            for (int j = i+1; j < vertex; j++) {
                if (matrix[i][j] != INF){
                    edges[index++] = new KGraph(data[i], data[j], matrix[i][j]);
                }
            }
        }
        return edges;
    }

    public int getNums(){
        int nums = 0;
        for (int i = 0; i < vertex; i++) {
            for (int j = i+1; j < vertex; j++) {
                if (matrix[i][j] != INF){
                    nums++;
                }
            }
        }
        return nums;
    }
    public int getEnds(int[] ends, int i){
        while (ends[i] != 0){
            i = ends[i];
        }
        return i;
    }
    public void sortEdges(KGraph[] edges){
        int nums = getNums();
        for (int i = 0; i < nums - 1; i++) {
            for (int j = 0; j < nums - i - 1; j++) {
                if (edges[j].weight > edges[j+1].weight){
                    KGraph temp = edges[j];
                    edges[j] = edges[j+1];
                    edges[j+1] = temp;
                }
            }
        }
    }

}
class KGraph{
    char start;
    char end;
    int weight;

    public KGraph(char start, char end, int weight) {
        this.start = start;
        this.end = end;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "KGraph{" +
                "start=" + start +
                ", end=" + end +
                ", weight=" + weight +
                '}';
    }
}

4.8 Dijkstra算法

应用场景-最短路径问题

看一个应用场景和问题:


 

1) 战争时期,胜利乡有 7 个村庄 (A, B, C, D, E, F, G) ,现在有六个邮差,从 G 点出发,需要分别把邮件分别送到 A, B, C , D, E, F 六个村庄
2) 各个村庄的距离用边线表示 ( ) ,比如 A – B 距离 5 公里
3) 问:如何计算出 G 村庄到 其它各个村庄的最短距离 ?
4) 如果从其它点出发到各个点的最短距离又是多少?

迪杰斯特拉(Dijkstra)算法介绍

迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止

迪杰斯特拉(Dijkstra)算法过程

        1)设置出发顶点为v,顶点集合V{v1,v2,vi...}vV中各顶点的距离构成距离集合DisDis{d1,d2,di...}Dis集合记录着v到图中各顶点的距离(到自身可以看作0vvi距离对应为di)

2) Dis 中选择值最小的 di 并移出 Dis 集合,同时移出 V 集合中对应的顶点 vi ,此时的 v vi 即为最短路径
3) 更新 Dis 集合,更新规则为:比较 v V 集合中顶点的距离值,与 v 通过 vi V 集合中顶点的距离值,保留值较小的一个 ( 同时也应该更新顶点的前驱节点为 vi ,表明是通过 vi 到达的 )
4) 重复执行两步骤,直到最短路径顶点为目标顶点即可结束

迪杰斯特拉(Dijkstra)算法最佳应用-最短路径代码实现:

public class Dijkstra {
    public static void main(String[] args) {
        char[] vertex = {'A','B','C','D','E','F','G'};
        final int N = 65535;
        int[][] matrix = {
                {N,5,7,N,N,N,2},
                {5,N,N,9,N,N,3},
                {7,N,N,N,8,N,N},
                {N,9,N,N,N,4,N},
                {N,N,8,N,N,5,4},
                {N,N,N,4,5,N,6},
                {2,3,N,N,4,6,N}
        };
        DGraph dGraph = new DGraph(vertex, matrix);
        dGraph.djs(2);
        dGraph.showDijs();
    }
}
class DGraph{
    char[] vertex;
    int[][] matrix;
    VisitVertex vv;

    public DGraph(char[] vertex, int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }
    public void showDijs(){
        vv.show();
    }
    public void update(int index){
        for (int i = 0; i < vertex.length; i++) {
            int len = vv.getDis(index) + matrix[index][i];
            if (!vv.isVisit(i) && len < vv.getDis(i)){
                vv.updateDis(i , len);
                vv.updatePre(index, i);
            }
        }
    }
    public void djs(int index){
        vv = new VisitVertex(vertex.length, index);
        update(index);
        for (int i = 1; i < vertex.length; i++) {
            index = vv.updateArr();
            update(index);
        }
    }


}
class VisitVertex{
    int[] already_arr;
    int[] pre_visit;
    int[]  dis;

    public VisitVertex(int length, int index) {
        this.already_arr = new int[length];
        this.pre_visit = new int[length];
        this.dis = new int[length];
        Arrays.fill(dis, 65535);
        already_arr[index] = 1;
        dis[index] = 0;
    }
    public boolean isVisit(int index){
        return already_arr[index] == 1;
    }
    public int getDis(int index){
        return dis[index];
    }
    public void updatePre(int index, int i){
        pre_visit[i] = index;
    }
    public void updateDis(int index, int len){
        dis[index] = len;
    }
    public int updateArr(){
        int min = 65535, index = 0;
        for (int i = 0; i < already_arr.length; i++) {
            if (already_arr[i] == 0 && dis[i] < min){
                min = dis[i];
                index = i;
            }
        }
        already_arr[index] = 1;
        return index;
    }
    public void show(){
        System.out.println();
        System.out.println(Arrays.toString(already_arr));
        System.out.println();
        System.out.println(Arrays.toString(pre_visit));
        System.out.println();
        System.out.println(Arrays.toString(dis));
        int count = 0;
        char[] vertex = {'A','B','C','D','E','F','G'};
        for (int di : dis) {
            if (di != 65535){
                System.out.println(vertex[count] + " : " + di);
            }else {
                System.out.println("N");
            }
            count++;
        }
    }
}

4.9 Floyd算法

弗洛伊德(Floyd)算法介绍

1) Dijkstra 算法一样,弗洛伊德 (Floyd) 算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、 1978 年图灵奖获得者、斯坦福大学计算机科学系教授 罗伯特 · 弗洛伊德 命名
2) 弗洛伊德算法 (Floyd) 计算图中各个顶点之间的最短路径
3) 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径。
4) 弗洛伊德算法 VS 迪杰斯特拉算法 :迪杰斯特拉算法通过 选定的被访问顶点 ,求出从出 发访问顶点到其他顶点的最短路径 ;弗洛伊德算法中 每一个顶点都是出发访问点 ,所以需要将每一个顶点看做被访问顶点,求出从 每一个顶点到其他顶点的最短路径

弗洛伊德(Floyd)算法图解分析

1) 设置顶点 vi 到顶点 vk 的最短路径已知为 Lik ,顶点 vk vj 的最短路径已知为 Lkj ,顶点 vi vj 的路径为 Lij ,则 vi vj 的最短路径为: min(( Lik+Lkj ), Lij ) vk 的取值为图中所有顶点,则可获得 vi vj 的最短路径
2) 至于 vi vk 的最短路径 Lik 或者 vk vj 的最短路径 Lkj ,是以同样的方式获得
3) 弗洛伊德 (Floyd) 算法图解分析 - 举例说明

弗洛伊德(Floyd)算法最佳应用-最短路径


 

1) 胜利乡有 7 个村庄 (A, B, C, D, E, F, G)
2) 各个村庄的距离用边线表示 ( ) ,比如 A – B 距离 5 公里
3)问:如何计算出各村庄 其它各村庄 的最短距离 ?

代码实现:

public class Floyd {
    public static void main(String[] args) {
        char[] vertex = {'A','B','C','D','E','F','G'};
        final int N = 65535;
        int[][] matrix = {
                {0,5,7,N,N,N,2},
                {5,0,N,9,N,N,3},
                {7,N,0,N,8,N,N},
                {N,9,N,0,N,4,N},
                {N,N,8,N,0,5,4},
                {N,N,N,4,5,0,6},
                {2,3,N,N,4,6,0}
        };
        FGraph fGraph = new FGraph(vertex, matrix);
        fGraph.floyd();
        fGraph.show();
    }
}
class FGraph{
    char[] vertex;
    int[][] matrix;
    int[][] pre;

    public FGraph(char[] vertex, int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
        this.pre = new int[vertex.length][vertex.length];
        for (int i = 0; i < vertex.length; i++) {
            Arrays.fill(pre[i], i);
        }
    }
    public void show(){
        System.out.println("前驱矩阵为:");
        for (int[] pres : pre) {
            System.out.println(Arrays.toString(pres));
        }
        System.out.println("路径图为:");
        for (int[] dis : matrix) {
            System.out.println(Arrays.toString(dis));
        }
        System.out.println();
        char[] vertex = {'A','B','C','D','E','F','G'};
        for (int i = 0; i < vertex.length; i++) {
            for (int j = i + 1; j < vertex.length; j++) {
                System.out.println(vertex[i] + " -> " + vertex[j] + " : " + matrix[i][j]);
            }
        }
    }
    public void floyd(){
        int len = 0;
        for (int k = 0; k < vertex.length; k++) {
            for (int i = 0; i < vertex.length; i++) {
                for (int j = 0; j < vertex.length; j++) {
                    len = matrix[i][k] + matrix[k][j];
                    if (len < matrix[i][j]){
                        matrix[i][j] = len;
                        pre[i][j] = pre[k][j];
                    }

                }
            }
        }
    }
}

4.10 马踏棋盘算法(骑士周游问题/dfs的应用)

马踏棋盘算法介绍和游戏演示

1) 马踏棋盘算法也被称为骑士周游问题
2) 将马随机放在国际象棋的 8×8 棋盘 Board[0 7][0 7] 的某个方格中,马按走棋规则 ( 马走日字 ) 进行移动。要求每个方格只进入一次,走遍棋盘上全部 64 个方格
3) 游戏演示  

马踏棋盘游戏代码实现

1) 马踏棋盘问题 ( 骑士周游问题 ) 实际上是图的深度优先搜索 (DFS) 的应用。
2) 如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了 53 个点,如图:走到了第 53 个,坐标( 1,0 ),发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回溯 …… ,思路分析 + 代码实现
3) 分析第一种方式的问题,并使用贪心算法( greedyalgorithm )进行优化。解决马踏棋盘问题 .
4) 使用前面的游戏来验证算法是否正确。
public class HorseChessboard {
    //    static int[][] chessboard;
    static boolean[] isVisit;
    static boolean finished;
    static int X;
    static int Y;

    public static void main(String[] args) {
        X = 8;
        Y = 8;
//        int row = 1;
//        int col = 1;
        int[][] chessboard = new int[X][Y];
        isVisit = new boolean[X * Y];
        System.out.println("算法开始运行……");
        long start = System.currentTimeMillis();
        visitChessBoard(chessboard, 0, 0, 1);
        long end = System.currentTimeMillis();
        System.out.println("共需要" + (end - start) + "ms");
//        for (int[] rows : chessboard) {
//            for (int col : rows) {
//                System.out.print(col + "\t");
//            }
//            System.out.println();
//        }
        for (int i = 0; i < chessboard.length; i++) {
            for (int j = 0; j < chessboard[0].length; j++) {
                System.out.print(chessboard[i][j] + "\t");
            }
            System.out.println();
        }
    }

    public static void visitChessBoard(int[][] chessboard, int row, int col, int step) {
        chessboard[row][col] = step;
        System.out.println(row + ", " + col + " : " + step);
        isVisit[row * X + col] = true;
        List<Point> next = next(new Point(row, col));
        sort(next);
        while (!next.isEmpty()) {
            Point p = next.remove(0);
            if (!isVisit[p.x * X + p.y]) {
                visitChessBoard(chessboard, p.x, p.y, step + 1);
            }
        }
        if (step < X * Y && !finished) {
            chessboard[row][col] = 0;
            isVisit[row * X + col] = false;
        } else {
            finished = true;
        }
    }

    public static List<Point> next(Point curPoints) {
        ArrayList<Point> ps = new ArrayList<>();
        Point p1 = new Point();



        if ((p1.x = curPoints.x + 2) < X && (p1.y = curPoints.y - 1) >= 0) {   //0             //
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoints.x + 2) < X && (p1.y = curPoints.y + 1) < Y) {    //1    
            ps.add(new Point(p1));
        }

        if ((p1.x = curPoints.x + 1) < X && (p1.y = curPoints.y + 2) < Y) {    //2         /
            ps.add(new Point(p1));
        }

        if ((p1.x = curPoints.x - 1) >= 0 && (p1.y = curPoints.y + 2) < Y) {    //3          
            ps.add(new Point(p1));
        }

        if ((p1.x = curPoints.x - 2) >= 0 && (p1.y = curPoints.y + 1) < Y) {    //4          ///
            ps.add(new Point(p1));
        }
        if ((p1.x = curPoints.x - 2) >= 0 && (p1.y = curPoints.y - 1) >= 0) {   //5     
            ps.add(new Point(p1));
        }

        if ((p1.x = curPoints.x - 1) >= 0 && (p1.y = curPoints.y - 2) >= 0) {    //6       ///
            ps.add(new Point(p1));
        }

        if ((p1.x = curPoints.x + 1) < X && (p1.y = curPoints.y - 2) >= 0) {     //7          ///
            ps.add(new Point(p1));
        }

        return ps;
    }


    public static void sort(List<Point> points) {
        points.sort(new Comparator<Point>() {
            @Override
            public int compare(Point o1, Point o2) {
                int count1 = next(o1).size();
                int count2 = next(o2).size();
                if (count1 < count2) {
                    return -1;
                } else if (count1 == count2) {
                    return 0;
                } else {
                    return -1;
                }
            }
        });
    }
}

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据结构与算法Java编程中扮演着非常重要的角色。数据结构是指在计机中存储和组织数据的方式,而算法则是指解决问题的步骤和策略。了解和掌握数据结构算法对于编写高效且可维护的Java程序至关重要。 在Java中,有许多常见的数据结构算法可以使用。比如数组、栈、队列、链表、树等。这些数据结构可以帮助我们在处理不同类型的数据时更加高效地存储和访问数据。同时,各种排序算法如冒泡排序、选择排序、插入排序、归并排序、快速排序等也是在Java编程中经常使用的算法。 通过学习和应用数据结构算法,我们可以提高程序的执行效率和性能。在面试中,数据结构算法的知识也是经常被考察的内容。因此,掌握Java中的数据结构算法对于提升编程能力和面试竞争力都非常重要。 引用提供了一些常用数据结构的介绍,包括数组、栈、队列、链表、树和图等。而引用则提供了一些常见的排序算法的介绍,如冒泡排序、选择排序、插入排序、归并排序、快速排序等。通过学习和实践这些数据结构算法,可以帮助我们更好地理解和应用于Java编程中。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [数据结构与算法详解(含算法分析、动图图解、Java代码实现、注释解析)](https://blog.csdn.net/yuan2019035055/article/details/120262225)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值