数据结构和算法---Java语言实现

数据结构和算法

文章目录

  • 数据结构和算法
    • 一、数据结构和算法概述
      • 1.1 什么是数据结构和算法
      • 1.2 数据结构分类
      • 1.2 算法的时间和空间复杂度
    • 二、排序算法
      • 2.1.冒泡排序
      • 2.2 选择排序
      • 2.3 插入排序
      • 2.4 希尔排序
      • 2.4 归并排序
      • 2.6 快速排序
    • 三、线性表
      • 3.1 线性表
      • 3.2 链表
      • 3.3 栈
      • 3.4 队列
    • 四、树
      • 4.1 树的相关定义
      • 4.2 二叉树
    • 五、堆
      • 5.1堆的定义
      • 5.2 代码实现
      • 5.3 堆排序
    • 六. 优先队列
    • 最大优先队列
    • 最小优先队列
    • 七、树的进阶
      • 7.1 平衡树
      • 7.2 红黑树
      • 7.3 B、B+ 树
    • 八、并查集
    • 九、图
      • 9.1 图的概述
      • 9.2 深度优先搜索
      • 9.3 广度优先搜索
      • 9.4 有向图
      • 9.5 拓扑排序
      • 9.6 最小生成树
      • 9.7 kruskal算法
      • 9.8 Dijiestra

一、数据结构和算法概述

1.1 什么是数据结构和算法

数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及他们之间的关系和操作等相关问题的学科。

就是把数据元素按照一定的关系组织起来的集合,用来组织和存储数据

1.2 数据结构分类

传统上,我们可以把数据结构分为逻辑结构物理结构两大类。

**逻辑结构分类: **

**集合结构:**集合结构中数据元素除了属于同一个集合外,他们之间没有任何其他的关系。

**线性结构:**线性结构中的数据元素之间存在一对一的关系

树形结构:树形结构中的数据元素之间存在一对多的层次关系

图形结构:图形结构的数据元素是多对多的关系

物理结构分类

逻辑结构在计算机中真正的表示方式(又称为映像)称为物理结构,也可以叫做存储结构。常见的物理结构有顺序存储结构链式存储结构

顺序存储结构:

把数据元素放到地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的 ,比如我们常用的数组就是顺序存储结构。

链式存储结构:

是把数据元素存放在任意的存储单元里面,这组存储单元可以是连续的也可以是不连续的。此时,数据元素之间并不能反映元素间的逻辑关系,因此在链式存储结构中引进了一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置

算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。

1.2 算法的时间和空间复杂度

算法函数中n最高次幂越小,算法效率越高

1.算法函数中的常数可以忽略;2.算法函数中最高次幂的常数因子可以忽略;3.算法函数中最高次幂越小,算法效率越高。

描述 增长的数量级 说明 举例
常数级别 1 普通语句 两个数相加
对数级别 logN 二分策略 二分查找
线性级别 N 循环 找最大值
线性对数级别 NlogN 分治 归并排序
平方级别 N^2 双层循环 检查所有元素对
立方级别 N^3 三层循环 检查所有三元组
指数级别 2^N 穷举查找 检查所有子集

复杂程度从低到高依次为:

O(1)2)3)

空间复杂度:一个字节是8位

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CpDxV763-1629866469951)(C:\Users\huo\AppData\Roaming\Typora\typora-user-images\image-20210817215917824.png)]

java中数组被被限定为对象,他们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组一般需要24字节的头信息(16个自己的对象开销,4字节用于保存长度以及4个填充字节)再加上保存值所需的内存。

二、排序算法

常用排序算法分析

时间复杂度 空间复杂度
类别 排序方法 平均情况 最好情况 最坏情况 辅助内存 稳定性
插入类 插入排序 O(N^2) O(N) O(N^2) O(1) 稳定
希尔排序 O(N^1.3-2) O(N) O(N^2) O(1) 不稳定
选择类 选择排序 O(N^2) O(N^2) O(N^2) O(1) 不稳定
堆排序 O(NlogN) O(NlogN) O(NlogN) O(1) 不稳定
交换类 冒泡排序 O(N^2) O(N) O(N^2) O(1) 稳定
快速排序 O(NlogN) O(NlogN) O(N^2) O(logn) 不稳定
归并排序 O(NlogN) O(NlogN) O(NlogN) O(n) 稳定
基数排序 O(d(r+n)) O(d(n+rd)) O(d(r+n)) O(rd+n) 稳定

r是关键字的基数,d是长度,n是关键字的个数

2.1.冒泡排序

public void bubbleSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        boolean flag = true;
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                flag = false;
            }
        }
        if (flag) break;
    }
}

冒泡排序的时间复杂度分析

冒泡排序使用了双层for循环,其中内层循环的循环体是真正完成排序的代码,所以,我们分析冒泡排序的时间复杂度,主要分析一下内层循环体的执行次数即可。在最坏情况下,也就是假如要排序的元素为{6,5,4,3,2,1}逆序,那么:元素比较的次数为:

(N-1)+(N-2)+(N-3)+…+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2;

元素交换的次数为:

(N-1)+(N-2)+(N-3)+…+2+1=((N-1)+1)*(N-1)/2=N^2/2-N/2;

总执行次数为:

(N2/2-N/2)+(N2/2-N/2)=N^2-N;

按照大O推导法则,保留函数中的最高阶项那么最终冒泡排序的时间复杂度为O(N^2)

2.2 选择排序

public void selectSort(int[] arr) {
    int index, min;
    for (int i = 0; i < arr.length; i++) {
        min = arr[i];
        index = i;
        for (int j = i + 1; j < arr.length; j++) {
            if (min > arr[j]) {
                min = arr[j];
                index = j;
            }
        }
        if (i != index) {
            arr[index] = arr[i];
            arr[i] = min;
        }
    }
}

2.3 插入排序

public void insertSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int val = arr[i], j = i - 1;
        for (; j >= 0 && arr[j] > val; j--) {
            arr[j + 1] = arr[j];
        }
        arr[j + 1] = val;
    }
}

2.4 希尔排序

排序原理:

1.选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;

  1. 对分好组的每一组数据完成插入排序;

3.减小增长量,最小减为1,重复第二步操作

public void shellSort(int[] arr) {
    for (int gap = arr.length / 2; gap > 0; gap--) {
        for (int i = gap; i < arr.length; i++) {
            int j = i, temp = arr[j];
            if (temp < arr[j - gap]) {
                while (j - gap >= 0 && temp < arr[j - gap]) {
                    arr[j] = arr[j - gap];
                    j -= gap;
                }
                arr[j] = temp;
            }
        }
    }
}

2.4 归并排序

public void mergeSort(int[] arr, int left, int right, int[] temp) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(arr, left, mid, temp);
        mergeSort(arr, mid + 1, right, temp);
        merge(arr, left, mid, right, temp);
    }
}

public void merge(int[] arr, int left, int mid, int right, int[] temp) {
    int i = left, j = mid + 1, t = 0, tempLeft;
    while (i <= mid && j <= right) {
        temp[t++] = arr[i] >= arr[j] ? arr[j++] : arr[i++];
    }
    while (i <= mid) {
        temp[t++] = arr[i++];
    }
    while (j <= right) {
        temp[t++] = arr[j++];
    }
    t = 0;
    tempLeft = left;
    while (tempLeft <= right) {
        arr[tempLeft++] = temp[t++];
    }
}

2.6 快速排序

public void quickSort(int[] arr, int left, int right) {
    if (left >= right) return;
    int i = left - 1, j = right + 1, mid = arr[(left + right) >> 1], temp;
    while (i < j) {
        do i++; while (i < j && arr[i] < mid);
        do j--; while (i < j && arr[j] > mid);
        if (i < j) {
            temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    quickSort(arr, left, j);
    quickSort(arr, j + 1, right);
}

三、线性表

3.1 线性表

get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1)

insert(int i,T t):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n);

remove(int i):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n);

由于顺序表的底层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的结点处,耗时会突增,尤其是元素越多,这个问题越明显

3.2 链表

单向链表

单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,

指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。

快慢指针可以解决链表是否有环问题,慢指针走一步,快指针走两步,如果相遇说明有环,当相遇后定义临时节点指向头节点,每次走一步,当与慢指针相遇时就是有环链表的入口

双向链表

双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。

循环链表

循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个节点的指针为null,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。

3.3 栈

栈是一种基于先进后出(FIFO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈.

代码实现(基于链表):

public class Stack<T>{
    private Node head;  //记录首结点
    private int N; //当前栈的元素个数
    
    private class Node {
        public T item;
        public Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
    
    public Stack() {
        this.head = new Node(null, null);
        this.N = 0;
    }

    public boolean isEmpty() {//判断栈是否为空,是返回true,否返回false
        return N == 0;
    }

    public int size() {//获取栈中元素的个数
        return N;
    }

    public void push(T t) { //向栈中压入元素;//记录首结点
        Node newNode = new Node(t, null);//创建压入的节点
        Node next = head.next; //存储头节点原来的下个节点
        head.next = newNode; //头节点指向新节点
        newNode.next = next; //新节点指向next
        N++;
    }

    public T pop() { //弹出栈顶元素
        if (head.next == null) {
            return null;
        }
        Node first = head.next;
        head.next = first.next;
        N--;
        return first.item;
    }
}

括号匹配问题:

public boolean isValid(String s) {
    if (s.length() % 2 != 0) return false;
    char[] arr = s.toCharArray();
    Stack<Character> stack = new Stack<>();
    for (char c : arr) {
        if (c == '(') stack.push(')');
        else if (c == '{') stack.push('}');
        else if (c == '[') stack.push(']');
        else if (stack.isEmpty() || stack.pop() != c) return false;
    }
    return stack.isEmpty();
}

栈应用—逆波兰表达式求值:

中缀表达式:

中缀表达式就是我们平常生活中使用的表达式,例如:1+3*2,2-(1+3)等等,中缀表达式的特点是:二元运算符置于两个操作数中间。

中缀表达式是人们最喜欢的表达式方式,因为简单,易懂。但是对于计算机来说就不是这样了,因为中缀表达式的运算顺序不具有规律性。不同的运算符具有不同的优先级,如果计算机执行中缀表达式,需要解析表达式语义,做大量的优先级相关操作。

逆波兰表达式(后缀表达式)

逆波兰表达式是波兰逻辑学家J・卢卡西维兹(J・ Lukasewicz)于1929年首先提出的一种表达式的表示方法,后缀表达式的特点:运算符总是放在跟它相关的操作数之后

public int evalRPN(String[] tokens) {
    Stack<Integer> stack = new Stack<>();
    for (String token : tokens) {
        if (!(token.equals("+") || token.equals("*")
                || token.equals("/") || token.equals("-"))) {
            stack.push(Integer.parseInt(token));
        } else {
            int a = stack.pop(), b = stack.pop();
            if (token.equals("-")) {
                stack.push(b - a);
            } else if (token.equals("+")) {
                stack.push(a + b);
            } else if (token.equals("*")) {
                stack.push(b * a);
            } else {
                stack.push(b / a);
            }
        }
    }
    return stack.pop();
}

3.4 队列

队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先读被读出来。

public class Queue<T>{
    private Node head;  //记录首结点
    private int N;  //当前栈的元素个数
    private Node last;  //记录最后一个结点

    private class Node {
        public T item;
        public Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }

    public Queue() {
        this.head = new Node(null, null);
        this.last = null;
        this.N = 0;
    }

    public boolean isEmpty() { //判断队列是否为空,是返回true,否返回false
        return N == 0;
    }

    public int size() { //获取队列中元素的个数
        return N;
    }

    public void add(T t) { //往队列中插入一个元素
        Node node = new Node(t, null);
        if (last == null) {
            last = node;
            head.next = last;
        } else {
            Node oldLast = last;
            last = node;
            oldLast.next = last;
        }
        N++;
    }

    public T pop() { //从队列中拿出一个元素
        if (isEmpty()) {
            return null;
        }
        Node next = head.next;
        head.next = next.next;
        N--;
        if (isEmpty()) {
            last = null;
        }
        return next.item;
    }
}

四、树

4.1 树的相关定义

树是我们计算机中非常重要的一种数据结构,同时使用树这种数据结构,可以描述现实生活中的很多事物,例如家谱、单位的组织架构、等等。树是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

树具有以下特点:

1.每个结点有零个或多个子结点;

2.没有父结点的结点为根结点;

3.每一个非根结点只有一个父结点;

4.每个结点及其后代结点整体上可以看做是一棵树,称为当前结点的父结点的一个子树;

树的相关术语

**结点的度:**一个结点含有的子树的个数称为该结点的度;

**叶结点:**度为0的结点称为叶结点,也可以叫做终端结点

**分支结点:**度不为0的结点称为分支结点,也可以叫做非终端结点

**结点的层次:**从根结点开始,根结点的层次为1,根的直接后继层次为2,以此类推

**结点的层序编号:**将树中的结点,按照从上层到下层,同层从左到右的次序排成一个线性序列,把他们编成连续的自然数。

**树的度:**树中所有结点的度的最大值

树的高度(深度):树中结点的最大层次

**森林:**m(m>=0)个互不相交的树的集合,将一颗非空树的根结点删去,树就变成一个森林;给森林增加一个统一的根

结点,森林就变成一棵树

4.2 二叉树

**基本定义:**二叉树就是度不超过2的树(每个结点最多有两个子结点)

**满二叉树:**一个二叉树,如果每一个层的结点树都达到最大值,则这个二叉树就是满二叉树

**完全二叉树:**叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树

二叉查找树代码实现:

public class BinaryTree<Key extends Comparable<Key>, Value> {

    private Node root;//记录根结点
    private int N;//记录树中元素的个数

    private class Node {
        public Key key;  //存储键
        private Value value;     // 存储值
        public Node left;  // 记录左子结点
        public Node right; // 记录右子结点

        public Node(Key key, Value value, Node left, Node right) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }

    public int size() {
        return N;
    }

    public void put(Key key, Value value) { //向树中添加元素
        root = put(root, key, value);
    }

    public Node put(Node x, Key key, Value value) { //向指定的树添加并返回
        //如果x为空
        if (x == null) { //树空
            N++;
            return new Node(key, value, null, null);
        }
        //x不为空
        int cmp = key.compareTo(x.key);
        if (cmp > 0) {
            x.right = put(x.right, key, value);
        } else if (cmp < 0) {
            x.left = put(x.left, key, value);
        } else {
            x.value = value;
        }
        return x;
    }

    public Value get(Key key) {
        return get(root, key);
    }

    public Value get(Node x, Key key) {
        if (x == null) {
            return null;//树空
        }
        int cmp = key.compareTo(x.key);
        if (cmp > 0) {
            return get(x.right, key);
        } else if (cmp < 0) {
            return get(x.left, key);
        } else {
            return x.value;
        }
    }

    public void delete(Key key) {
        delete(root, key);
    }

    public Node delete(Node x, Key key) {
        if (x == null) {
            return null;//树空
        }
        int cmp = key.compareTo(x.key);
        if (cmp > 0) {
            x.right = delete(x.right, key);
        } else if (cmp < 0) {
            x.left = delete(x.left, key);
        } else {
            N--;
            //找到了要删除的节点
            //如果要删除节点的左右树为空的话
            if (x.right == null) {
                return x.left;
            }
            if (x.left == null) {
                return x.right;
            }
            //不为空 找到右子树中最小的节点 (即一直找右子树的左子树)
            Node minNode = x.right;
            while (minNode.left != null) {
                minNode = minNode.left;
            }
            //找到后 删除右子树中最小的节点
            Node n = x.right;
            while (n.left != null) {
                if (n.left.left != null) {
                    n.left = null;
                } else {
                    n = n.left;
                }
            }
            minNode.left = x.left;
            minNode.right = x.right;
            x = minNode;//要删除节点的父节点指向minNode
        }
        return x;
    }
}

五、堆

5.1堆的定义

堆是计算机科学中一类特殊的数据结构的统称,堆通常可以被看做是一棵完全二叉树的数组对象。

堆的特性:

1.它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。

2.它通常用数组来实现。

具体方法就是将二叉树的结点按照层级顺序放入数组中,根结点在位置1,它的子结点在位置2和3,而子结点的子结点则分别在位置4,5,6和7,以此类推

如果一个结点的位置为k,则它的父结点的位置为**[k/2],而它的两个子结点的位置则分别为2k和2k+1**。这样,在不使用指针的情况下,我们也可以通过计算数组的索引在树中上下移动:从a[k]向上一层,就令k等于k/2,向下一层就令k等于2k或2k+1。

3.每个结点都大于等于它的两个子结点。这里要注意堆中仅仅规定了每个结点大于等于它的两个子结点,但这两个子结点的顺序并没有做规定,跟我们之前学习的二叉查找树是有区别的。

5.2 代码实现

public class Heap<T extends Comparable<T>> {
    private T[] items;
    private int N;

    public Heap(int capacity) {
        this.items = (T[]) new Comparable[capacity + 1];
        this.N = 0;
    }

    private boolean less(int i, int j) {
        return items[i].compareTo(items[j]) < 0;
    }

    private void exch(int i, int j) {
        T temp = items[i];
        items[i] = items[j];
        items[j] = temp;
    }

    public void insert(T t) { //往堆中插入元素
        items[++N] = t;
        swim(N);
    }

    private void swim(int k) { //上浮 使索k处元素位于正确位置
        while (k > 1) {
            //比较当前和节点和父节点
            if (less(k / 2, k)) { //如果父节点比他小
                exch(k / 2, k);
            }
            k = k / 2;
        }
    }

    //删除堆中最大元素
    public T delMax() {
        T max = items[1];
        exch(1, N); //交换索引1的元素和最大索引的元素
        items[N] = null; //删除最大元素
        N--;
        sink(1); //通过下沉 让堆重新有序
        return max;
    }

    private void sink(int k) {
        ///循环对比当前节点和两个子节点(2*k,2*k+1)
        while (2 * k <= N) {
            int max; //获取子节点的较大值索引
            if (2 * k + 1 <= N) { //有右子节点
                if (less(2 * k, 2 * k + 1)) {
                    max = 2 * k + 1;
                } else {
                    max = 2 * k;
                }
            } else {
                max = 2 * k;
            }
            if (!less(k, max)) {
                break;
            }
            //获取后与当前节点交换
            exch(k, max);
            k = max;
        }
    }
}

5.3 堆排序

实现步骤:

1.构造堆;

2.得到堆顶元素,这个值就是最大值;

3.交换堆顶元素和数组中的最后一个元素,此时所有元素中的最大元素已经放到合适的位置;

4.对堆进行调整,重新让除了最后一个元素的剩余元素中的最大值放到堆顶;

5.重复2~4这个步骤,直到堆中剩一个元素为止。

堆构造过程

堆的构造,最直观的想法就是另外再创建一个和新数组数组,然后从左往右遍历原数组,每得到一个元素后,添加到新数组中,并通过上浮,对堆进行调整,最后新的数组就是一个堆。

上述的方式虽然很直观,也很简单,但是我们可以用更聪明一点的办法完成它。创建一个新数组,把原数组0length-1的数据拷贝到新数组的1length处,再从新数组长度的一半处开始往1索引处扫描(从右往左),然后对扫描到的每一个元素做下沉调整即可。

堆排序过程

对构造好的堆,我们只需要做类似于堆的删除操作,就可以完成排序。

1.将堆顶元素和堆中最后一个元素交换位置;

2.通过对堆顶元素下沉调整堆,把最大的元素放到堆顶(此时最后一个元素不参与堆的调整,因为最大的数据已经到了数组的最右边)

3.重复1~2步骤,直到堆中剩最后一个元素。

代码实现:

public class HeapSort {
    private static boolean less(Comparable[] heap, int i, int j) {//判断heap堆中索引i处的元素是否小于索引j处的元素
        return heap[i].compareTo(heap[j]) < 0;
    }

    private static void exch(Comparable[] heap, int i, int j) {//交换heap堆中i索引和j索引处的值
        Comparable temp = heap[i];
        heap[i] = heap[j];
        heap[j] = temp;
    }

    private static void createHeap(Comparable[] source, Comparable[] heap) {//根据原数组source,构造出堆heap
        System.arraycopy(source, 0, heap, 1, source.length);
        for (int i = heap.length / 2; i > 0; i--) {
            sink(heap, i, heap.length - 1);
        }
    }

    public static void sort(Comparable[] source) {//对source数组中的数据从小到大排序
        Comparable[] heap = new Comparable[source.length + 1];
        createHeap(source, heap);
        int N = heap.length - 1;
        while (N != 1) {
            exch(heap, 1, N);
            N--;
            sink(heap, 1, N);
        }
        System.arraycopy(heap, 1, source, 0, source.length);
    }

    private static void sink(Comparable[] heap, int target, int range) { 
        //在heap堆中,对target处的元素做下沉,范围是0~range。
        while (2 * target <= range) {
            //找出当前节点的较大子节点
            int max;
            if (2 * target + 1 <= range) {
                if (less(heap, 2 * target, 2 * target + 1)) {
                    max = 2 * target + 1;
                } else {
                    max = 2 * target;
                }
            } else {
                max = 2 * target;
            }
            if (!less(heap, target, max)) {
                break;
            }
            exch(heap, target, max);
            target = max;
        }
    }
}

六. 优先队列

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在某些情况下,我们可能需要找出队列中的最大值或者最小值,例如使用一个队列保存计算机的任务,一般情况下计算机的任务都是有优先级的,我们需要在这些计算机的任务中找出优先级最高的任务先执行,执行完毕后就需要把这个任务从队列中移除。普通的队列要完成这样的功能,需要每次遍历队列中的所有元素,比较并找出最大值,效率不是很高,这个时候,我们就可以使用一种特殊的队列来完成这种需求,优先队列。

优先队列按照其作用不同,可以分为以下两种:

最大优先队列

可以获取并删除队列中最大的值

最大的元素放在数组的索引1处。

每个结点的数据总是大于等于它的两个子结点的数据。

public class MaxPriorityQueue<T extends Comparable<T>> {
    private T[] items;
    private int N;

    public MaxPriorityQueue(int capacity) {
        this.items = (T[]) new Comparable[capacity + 1];
        this.N = 0;
    }

    //获取队列中元素的个数
    public int size() {
        return N;
    }

    //判断队列是否为空
    public boolean isEmpty() {
        return N == 0;
    }

    //判断堆中索引i处的元素是否小于索引j处的元素
    private boolean less(int i, int j) {
        return items[i].compareTo(items[j]) < 0;
    }

    //交换堆中i索引和j索引处的值
    private void exch(int i, int j) {
        T tmp = items[i];
        items[i] = items[j];
        items[j] = tmp;
    }

    //往堆中插入一个元素
    public void insert(T t) {
        items[++N] = t;
        swim(N);
    }

    //删除堆中最大的元素,并返回这个最大元素
    public T delMax() {
        T max = items[1];
        //交换索引1处和索引N处的值
        exch(1, N);
        items[N] = null; //删除最后位置上的元素
        N--;  //个数-1
        sink(1);
        return max;
    }

    //上浮算法 使索引k处的元素能在堆中处于一个正确的位置
    private void swim(int k) {
        while (k > 1) {
            if (less(k / 2, k)) {
                exch(k / 2, k);
            }
            k = k / 2;
        }
    }

    private void sink(int k) {
        while (2 * k <= N) {
            int max; //获取子节点的较大值索引
            if (2 * k + 1 <= N) { //有右子节点
                if (less(2 * k, 2 * k + 1)) {
                    max = 2 * k + 1;
                } else {
                    max = 2 * k;
                }
            } else {
                max = 2 * k;
            }
            if (!less(k, max)) {
                break;
            }
            exch(k, max);
            k = max;
        }
    }
}

最小优先队列

可以获取并删除队列中最小的值

最小的元素放在数组的索引1处。

每个结点的数据总是小于等于它的两个子结点的数据。

public class MinPriorityQueue<T extends Comparable<T>> {
    private T[] items;
    private int N;

    public MinPriorityQueue(int capacity) {
        this.items = (T[]) new Comparable[capacity + 1];
        this.N = 0;
    }

    //获取队列中元素的个数
    public int size() {
        return N;
    }

    //判断队列是否为空
    public boolean isEmpty() {
        return N == 0;
    }

    //判断堆中索引i处的元素是否小于索引j处的元素
    private boolean less(int i, int j) {
        return items[i].compareTo(items[j]) < 0;
    }

    //交换堆中i索引和j索引处的值
    private void exch(int i, int j) {
        T tmp = items[i];
        items[i] = items[j];
        items[j] = tmp;
    }

    //往堆中插入一个元素
    public void insert(T t) {
        items[++N] = t;
        swim(N);
    }

    //删除堆中最小的元素,并返回这个最小元素
    public T delMin() {
        T min = items[1];
        //交换索引1处和索引N处的值
        exch(1, N);
        items[N] = null; //删除最后位置上的元素
        N--;  //个数-1
        sink(1);
        return min;
    }

    //上浮算法 使索引k处的元素能在堆中处于一个正确的位置
    private void swim(int k) {
        while (k > 1) {
            if (less(k, k / 2)) {//如果当前节点比父节点小
                exch(k, k / 2);
            }
            k = k / 2;
        }
    }

    private void sink(int k) {
        while (2 * k <= N) {
            int min; //获取子节点的较小值索引
            if (2 * k + 1 <= N) { //有右子节点
                if (less(2 * k, 2 * k + 1)) {
                    min = 2 * k;
                } else {
                    min = 2 * k + 1;
                }
            } else {
                min = 2 * k;
            }
            if (less(k, min)) {
                break;
            }
            exch(k, min);
            k = min;
        }
    }
}

七、树的进阶

7.1 平衡树

2-3查找树

为了保证查找树的平衡性,我们需要一些灵活性,因此在这里我们允许树中的一个结点保存多个键。确切的说,我们将一棵标准的二叉查找树中的结点称为2-结点(含有一个键和两条链),而现在我们引入3-结点,它含有两个键和三条链。2-结点和3-结点中的每条链都对应着其中保存的键所分割产生的一个区间。

2-3查找树的定义

一棵2-3查找树要么为空,要么满足满足下面两个要求:

2-结点:

含有一个键(及其对应的值)和两条链,左链接指向2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。

3-结点:

含有两个键(及其对应的值)和三条链,左链接指向的2-3树中的键都小于该结点,中链接指向的2-3树中的键都位于该结点的两个键之间,右链接指向的2-3树中的键都大于该结点。

一棵完全平衡的2-3树具有以下性质:

  1. 任意空链接到根结点的路径长度都是相等的。

  2. 4-结点变换为3-结点时,树的高度不会发生变化,只有当根结点是临时的4-结点,分解根结点时,树高+1。

  3. 2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长,而2-3树是自底向上生长。

7.2 红黑树

红黑树主要是对2-3树进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。我们将树中的链接分为两种类型:

**红链接:**将两个2-结点连接起来构成一个3-结点; **黑链接:**则是2-3树中的普通链接。

确切的说,我们将3-结点表示为由由一条左斜的红色链接(两个2-结点其中之一是另一个的左子结点)相连的两个2-结点。这种表示法的一个优点是,我们无需修改就可以直接使用标准的二叉查找树的get方法。

红黑树是含有红黑链接并满足下列条件的二叉查找树:

  1. 红链接均为左链接
  2. 没有任何一个结点同时和两条红链接相连;
  3. 该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同;

**左旋:**当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋。

右旋:当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋

public class RedBlackTree<Key extends Comparable<Key>, Value> {
    private Node root;
    private int N;
    private static final boolean RED = true; //红色链接
    private static final boolean BLACK = false; //黑色链接

    private class Node {
        public Key key;
        private Value value;
        public Node left;
        public Node right;
        public boolean color;

        public Node(Key key, Value value, Node left, Node right, boolean color) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
            this.color = color;
        }
    }

    public int size() {
        return N;
    }

    public boolean isRed(Node x) { //判断当前节点的父指向链接是否为红色
        if (x == null) return false;
        return x.color == RED;
    }

    //左旋转
    private Node rotateLeft(Node h) {
        Node x = h.right;
        h.right = x.left;
        x.left = h;
        x.color = h.color;
        h.color = RED;
        return x;
    }

    //右旋转
    private Node rotateRight(Node h) {
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        x.color = h.color;
        h.color = RED;
        return x;
    }

    //颜色反转 相当于完成拆分4-节点
    private void flipColors(Node h) {
        h.color = RED;
        h.left.color = BLACK;
        h.right.color = BLACK;
    }

    //在整个树上完成插入操作
    public void put(Key key, Value val) {
        root = put(root, key, val);
        root.color = BLACK;
    }

    public Node put(Node h, Key key, Value val) {
        if (h == null) {
            N++;
            return new Node(key, val, null, null, RED);
        }
        //比较h节点的键和key的大小
        int cmp = key.compareTo(h.key);
        if (cmp < 0) { //继续往左
            h.left = put(h.left, key, val);
        } else if (cmp > 0) { //继续往右
            h.right = put(h.right, key, val);
        } else {
            h.value = val;
        }
        //进行左旋
        if (isRed(h.right) && !isRed(h.left)) {
            h = rotateLeft(h);
        }
        //进行右旋
        if (isRed(h.left) && isRed(h.left.left)) {
            h = rotateRight(h);
        }
        //颜色反转
        if (isRed(h.left) && isRed(h.right)) {
            flipColors(h);
        }
        return h;
    }

    public Value get(Key key) {
        return get(root, key);
    }

    public Value get(Node x, Key key) {
        if (x == null) return null;
        int cmp = key.compareTo(x.key);
        if (cmp < 0) {
            return get(x.left, key);
        } else if (cmp > 0) {
            return get(x.right, key);
        } else {
            return x.value;
        }
    }
}

7.3 B、B+ 树

B树是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等操作。

B树的特性

B树中允许一个结点中包含多个key,可以是3个、4个、5个甚至更多,并不确定,需要看具体的实现。现在我们选择一个参数M,来构造一个B树,我们可以把它称作是M阶的B树,那么该树会具有如下特点:

每个结点最多有M-1个key,并且以升序排列;

每个结点最多能有M个子结点;

根结点至少有两个子结点;

在实际应用中B树的阶数一般都比较大(通常大于100),所以,即使存储大量的数据,B树的高度仍然比较小,这样在某些应用场景下,就可以体现出它的优势。

B+树

B+树是对B树的一种变形树,它与B树的差异在于:

  1. 非叶结点仅具有索引作用,也就是说,非叶子结点只存储key,不存储value;

  2. 树的所有叶结点构成一个有序链表,可以按照key排序的次序遍历全部数据。

B+树和B树的对比

B+ 树的优点在于:

1.由于B+树在非叶子结点上不包含真正的数据,只当做索引使用,因此在内存相同的情况下,能够存放更多的key。

2.B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。

B树的优点在于:

由于B树的每一个节点都包含key和value,因此我们根据key查找value时,只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找,都必须一次一次,一直找到树的最大深度处,也就是叶子结点的深度,才能找到value。

八、并查集

并查集是一种树型的数据结构 ,并查集可以高效地进行如下操作:

查询元素p和元素q是否属于同一组、合并元素p和元素q所在的组

public class UF_Tree_Weighted {
    private int[] eleAndGroup; //记录节点元素和该元素所在分组的标识
    private int count;
    private int[] sz;

    public UF_Tree_Weighted(int N) {
        this.count = N;
        eleAndGroup = new int[N];
        for (int i = 0; i < eleAndGroup.length; i++) {
            eleAndGroup[i] = i;
        }
        this.sz = new int[N];
        for (int i = 0; i < sz.length; i++) {
            sz[i] = 1;
        }
    }

    public int count() {
        return count;
    }

    public int find(int p) {
        while (true) {
            if (p == eleAndGroup[p]) return p;
            p = eleAndGroup[p];
        }
    }

    public boolean connected(int p, int q) {
        return find(p) == find(q);
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot == qRoot) return;
        if (sz[pRoot] < sz[qRoot]) {
            eleAndGroup[pRoot] = qRoot;
            sz[qRoot] += sz[pRoot];
        } else {
            eleAndGroup[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
        this.count--;
    }
}

九、图

9.1 图的概述

**定义:**图是由一组顶点和一组能够将两个顶点相连的边组成的

图的分类:

按照连接两个顶点的边的不同,可以把图分为以下两种:

无向图:边仅仅连接两个顶点,没有其他含义;

有向图:边不仅连接两个顶点,并且具有方向;

图的相关术语

**相邻顶点:**当两个顶点通过一条边相连时,我们称这两个顶点是相邻的,并且称这条边依附于这两个顶点。

**度:**某个顶点的度就是依附于该顶点的边的个数

**子图:**是一幅图的所有边的子集(包含这些边依附的顶点)组成的图;

**路径:**是由边顺序连接的一系列的顶点组成

**环:**是一条至少含有一条边且终点和起点相同的路径

**连通图:**如果图中任意一个顶点都存在一条路径到达另外一个顶点,那么这幅图就称之为连通图

**连通子图:**一个非连通图由若干连通的部分组成,每一个连通的部分都可以称为该图的连通子图

存储方式: 1.邻接矩阵

使用一个V*V的二维数组int[V] [V]adj,把索引的值看做是顶点;

如果顶点v和顶点w相连,我们只需要将adj[v] [w]和adj[w] [v]的值设置为1,否则设置为0即可

邻接表

1.使用一个大小为V的数组 Queue[V] adj,把索引看做是顶点;

2.每个索引处adj[v]存储了一个队列,该队列中存储的是所有与该顶点相邻的其他顶点

9.2 深度优先搜索

所谓的深度优先搜索,指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找子结点,然后找兄弟结点

//图的深度优先搜索
public class DepthFirstSearch {
    private boolean[] marked; //索引代表顶点,值表示当前顶点是否已经被搜索
    private int count; //记录图中有多少个顶点与s顶点相通

    public DepthFirstSearch(Graph G, int s) {
        this.marked = new boolean[G.V()];
        this.count = 0;
        dfs(G, s);
    }

    private void dfs(Graph G, int v) {//找到G图中V顶点的所有相通顶点
        marked[v] = true;
        for (Integer w : G.adj(v)) {
            if (!marked[w]) {
                dfs(G, w);
            }
        }
        count++;
    }

    public boolean marked(int w) {
        return marked[w];
    }

    public int count() {
        return count;
    }
}

9.3 广度优先搜索

所谓的深度优先搜索,指的是在搜索时,如果遇到一个结点既有子结点,又有兄弟结点,那么先找兄弟结点,然后找子结点。

//图的广度优先搜索
public class BreadthFirstSearch {

    private boolean[] marked; //索引代表顶点,值表示当前顶点是否已经被搜索
    private int count; // 记录有多少个顶点与s顶点相通
    private Queue<Integer> waitSearch; // 用来存储待搜索邻接表的点

    public BreadthFirstSearch(Graph G, int s) {
        this.marked = new boolean[G.V()];
        this.count = 0;
        this.waitSearch = new Queue<>();
        bfs(G, s);
    }

    private void bfs(Graph G, int v) {
        marked[v] = true;
        waitSearch.add(v);
        while (!waitSearch.isEmpty()) {
            Integer wait = waitSearch.pop();
            for (Integer w : G.adj(wait)) {
                if (!marked[w]) {
                    marked[w] = true;
                    waitSearch.add(w);
                    count++;
                }
            }
        }
    }

    public boolean marked(int w) {
        return marked[w];
    }

    public int count() {
        return count;
    }
}

9.4 有向图

定义:

有向图是一副具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点。

出度:

由某个顶点指出的边的个数称为该顶点的出度。

入度:

指向某个顶点的边的个数称为该顶点的入度。

有向路径:

由一系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点。

有向环:

一条至少含有一条边,且起点和终点相同的有向路径。

public class Digraph {
    private final int V; //顶点数目
    private int E; //边的数目
    private Queue<Integer>[] adj; //邻接表

    public Digraph(int v) {
        this.V = v;
        this.E = 0;
        this.adj = new Queue[V];
        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<>();
        }
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    public void addEdge(int v, int w) {
        adj[v].add(w);
        E++;
    }

    public Queue<Integer> adj(int v) {
        return adj[v];
    }

    private Digraph reverse() {
        Digraph r = new Digraph(V);
        for (int i = 0; i < V; i++) {
            for (Integer w : adj[i]) {
                r.addEdge(w, i);
            }
        }
        return r;
    }
}

9.5 拓扑排序

给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素,此时就可以明确的表示出每个顶点的优先级

**检测有向图中的环 ** 当我们深度搜索时:

在如果当前顶点正在搜索,则把对应的onStack数组中的值改为true,标识进栈;

如果当前顶点搜索完毕,则把对应的onStack数组中的值改为false,标识出栈;

如果即将要搜索某个顶点,但该顶点已经在栈中,则图中有环;

public class DirectedCycle {
    //索引代表顶点,值表示当前顶点是否已经被搜索
    private boolean[] marked;
    //记录图中是否有环
    private boolean hasCycle;
    // 索引代表顶点,使用栈的思想,记录当前顶点有没有已经处于正在搜索的有向路径上
    private boolean[] onStack;

    public DirectedCycle(Digraph G) {
        this.marked = new boolean[G.V()];
        this.hasCycle = false;
        this.onStack = new boolean[G.V()];
        for (int i = 0; i < G.V(); i++) {
            if (!marked[i]) {
                dfs(G, i);
            }
        }
    }

    public void dfs(Digraph G, int v) { //深度优先搜索判断图中是否有环
        marked[v] = true;
        onStack[v] = true;
        for (Integer w : G.adj(v)) {
            if (!marked[w]) {
                dfs(G, w);
            }
            if (onStack[w]) {
                hasCycle = true;
                return;
            }
        }
        onStack[v] = false;
    }

    public boolean hasCycle() {
        return hasCycle;
    }
}

基于深度优先的顶点排序

添加了一个栈reversePost用来存储顶点,当我们深度搜索图时,每搜索完毕一个顶点,把该顶点放入到reversePost中,这样就可以实现顶点排序

public class DepthFirstOrder {
    //索引代表顶点,值表示当前顶点是否已经被搜索
    private boolean[] marked;
    // 使用栈,存储顶点序列
    private Stack<Integer> reversePost;

    public DepthFirstOrder(Digraph G) {
        this.marked = new boolean[G.V()];
        this.reversePost = new Stack<>();
        for (int v = 0; v < G.V(); v++) {
            if (!marked[v]) {
                dfs(G, v);
            }
        }
    }

    private void dfs(Digraph G, int v) {
        marked[v] = true;
        for (Integer w : G.adj(v)) {
            if (!marked[w]) {
                dfs(G, w);
            }
        }
        reversePost.push(v);
    }

    public Stack<Integer> reversePost() {
        return reversePost;
    }
}

拓扑排序实现

public class TopoLogical {
    private Stack<Integer> order;

    public TopoLogical(Digraph G) {
        DirectedCycle cycle = new DirectedCycle(G);
        if (!cycle.hasCycle()) {s
            DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G);
            order = depthFirstOrder.reversePost();
        }
    }

    private boolean isCycle() {
        return order == null;
    }

    public Stack<Integer> order() {
        return order;
    }
}

9.6 最小生成树

图的生成树是它的一棵含有其所有顶点的无环连通子图,一副加权无向图的最小生成树它的一棵权值(树中所有边的权重之和)最小的生成树

public class PrimMST {
    private Edge[] edgeTo;
    private double[] disTo;
    private boolean[] marked;
    private IndexMinPriorityQueue<Double> pq;

    public PrimMST(EdgeWeightedGraph G) {
        this.edgeTo = new Edge[G.V()];
        this.disTo = new double[G.V()];
        for (int i = 0; i < disTo.length; i++) {
            disTo[i] = Double.POSITIVE_INFINITY;
        }
        this.marked = new boolean[G.V()];
        pq = new IndexMinPriorityQueue<>(G.V());
        disTo[0] = 0.0;
        pq.insert(0, 0.0);
        while (!pq.isEmpty()) {
            visit(G, pq.delMin());
        }
    }

    private void visit(EdgeWeightedGraph G, int v) {
        marked[v] = true;
        for (Edge e : G.adj(v)) {
            int w = e.other(v);
            if (marked[w]) {
                continue;
            }
            if (e.weight() < disTo[w]) {
                edgeTo[w] = e;
                disTo[w] = e.weight();
                if (pq.contains(w)) {
                    pq.changeItem(w, e.weight());
                } else {
                    pq.insert(w, e.weight());
                }
            }
        }
    }

    public Queue<Edge> edges() {
        Queue<Edge> edges = new Queue<>();
        for (int i = 0; i < edgeTo.length; i++) {
            if (edgeTo[i] != null) {
                edges.add(edgeTo[i]);
            }
        }
        return edges;
    }
}

9.7 kruskal算法

在设计API的时候,使用了一个MinPriorityQueue pq存储图中所有的边,每次使用pq.delMin()取出权重最小的边,并得到该边关联的两个顶点v和w,通过uf.connect(v,w)判断v和w是否已经连通,如果连通,则证明这两个顶点在同一棵树中,那么就不能再把这条边添加到最小生成树中,因为在一棵树的任意两个顶点上添加一条边,都会形成环,而最小生成树不能有环的存在,如果不连通,则通过uf.connect(v,w)把顶点v所在的树和顶点w所在的树合并成一棵树,并把这条边加入到mst队列中,这样如果把所有的边处理完,最终mst中存储的就是最小生树的所有边。

public class KruskalMST {
    //保存最小生成树的所有边
    private Queue<Edge> mst;
    //索引代表顶点,使用uf.connect(v,w)可以判断顶点v和顶点w是否在同一颗树中,使用uf.union(v,w)可以 把顶点v所在的树和顶点w所在的树合并
    private UF_Tree_Weighted uf;
    // 存储图中所有的边,使用最小优先队列,对边按照权重进行排序
    private MinPriorityQueue<Edge> pq;

    public KruskalMST(EdgeWeightedGraph G) {
        this.mst = new Queue<>();
        this.uf = new UF_Tree_Weighted(G.V());
        this.pq = new MinPriorityQueue<>(G.E() + 1);
        for (Edge e : G.edges()) {
            pq.insert(e);
        }
        while (!pq.isEmpty() && mst.size() < G.V() - 1) {
            Edge e = pq.delMin();
            int v = e.either();
            int w = e.other(v);
            if (uf.connected(v, w)) {
                continue;
            }
            uf.union(v, w);
            mst.add(e);
        }
    }

    public Queue<Edge> edges() {
        return mst;
    }
}

加权有向图:

public class EdgeWeightedDigraph {
    //顶点总数
    private final int V;
    //边的总数
    private int E;
    //邻接表
    private Queue<DirectedEdge>[] adj;

    //创建一个含有V个顶点的空加权有向图
    public EdgeWeightedDigraph(int V) {
        //初始化顶点数量
        this.V = V;
        //初始化边的数量
        this.E = 0;
        //初始化邻接表
        this.adj = new Queue[V];

        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<DirectedEdge>();
        }
    }

    //获取图中顶点的数量
    public int V() {
        return V;
    }

    //获取图中边的数量
    public int E() {
        return E;
    }


    //向加权有向图中添加一条边e
    public void addEdge(DirectedEdge e) {
        //边e是有方向的,所以只需要让e出现在起点的邻接表中即可
        int v = e.from();
        adj[v].add(e);
        E++;
    }

    //获取由顶点v指出的所有的边
    public Queue<DirectedEdge> adj(int v) {
        return adj[v];
    }

    //获取加权有向图的所有边
    public Queue<DirectedEdge> edges() {
        //遍历图中的每一个顶点,得到该顶点的邻接表,遍历得到每一条边,添加到队列中返回即可
        Queue<DirectedEdge> allEdges = new Queue<>();
        for (int v = 0; v < V; v++) {
            for (DirectedEdge edge : adj[v]) {
                allEdges.add(edge);
            }
        }
        return allEdges;
    }
}

9.8 Dijiestra

Disjstra算法的实现和Prim算法很类似,构造最短路径树的每一步都是向这棵树中添加一条新的边,而这条新的边

是有效横切边pq队列中的权重最小的边。

public class DijkstraSP {
    //索引代表顶点,值表示从顶点s到当前顶点的最短路径上的最后一条边
    private DirectedEdge[] edgeTo;
    //索引代表顶点,值从顶点s到当前顶点的最短路径的总权重
    private double[] distTo;
    //存放树中顶点与非树中顶点之间的有效横切边
    private IndexMinPriorityQueue<Double> pq;

    //根据一副加权有向图G和顶点s,创建一个计算顶点为s的最短路径树对象
    public DijkstraSP(EdgeWeightedDigraph G, int s) {
        //初始化edgeTo
        this.edgeTo = new DirectedEdge[G.V()];
        //初始化distTo
        this.distTo = new double[G.V()];
        for (int i = 0; i < distTo.length; i++) {
            distTo[i] = Double.POSITIVE_INFINITY;
        }
        //初始化pq
        this.pq = new IndexMinPriorityQueue<>(G.V());
        //找到图G中以顶点s为起点的最短路径树
        //默认让顶点s进入到最短路径树中
        distTo[s] = 0.0;
        pq.insert(s, 0.0);
        //遍历pq
        while (!pq.isEmpty()) {
            relax(G, pq.delMin());
        }
    }

    //松弛图G中的顶点v
    private void relax(EdgeWeightedDigraph G, int v) {

        for (DirectedEdge edge : G.adj(v)) {
            //获取到该边的终点w
            int w = edge.to();
            //通过松弛技术,判断从起点s到顶点w的最短路径是否需要先从顶点s到顶点v,然后再由顶点v到顶点w
            if (distTo(v) + edge.weight() < distTo(w)) {
                distTo[w] = distTo[v] + edge.weight();
                edgeTo[w] = edge;
                //判断pq中是否已经存在顶点w,如果存在,则更新权重,如果不存在,则直接添加
                if (pq.contains(w)) {
                    pq.changeItem(w, distTo(w));
                } else {
                    pq.insert(w, distTo(w));
                }
            }
        }
    }

    //获取从顶点s到顶点v的最短路径的总权重
    public double distTo(int v) {
        return distTo[v];
    }

    //判断从顶点s到顶点v是否可达
    public boolean hasPathTo(int v) {
        return distTo[v] < Double.POSITIVE_INFINITY;
    }

    //查询从起点s到顶点v的最短路径中所有的边
    public Queue<DirectedEdge> pathTo(int v) {
        //判断从顶点s到顶点v是否可达,如果不可达,直接返回null
        if (!hasPathTo(v)) {
            return null;
        }
        //创建队列对象
        Queue<DirectedEdge> allEdges = new Queue<>();

        while (true) {
            DirectedEdge e = edgeTo[v];
            if (e == null) {
                break;
            }
            allEdges.add(e);
            v = e.from();
        }
        return allEdges;
    }
}

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