【Java数据结构和算法】

目录

介绍

数据结构&算法的定义

数据结构和算法的关系

数据结构与算法整体知识

思维导图

数据结构的分类

逻辑结构

物理结构

算法的分析

  时间复杂度分析

 空间复杂度分析

 排序算法

冒泡排序

排序原理:

冒泡排序API设计

选择排序 

排序原理:

API设计

插入排序

 排序原理:

 API设计

希尔排序 

排序原理

 希尔排序的API设计

归并排序 

排序原理:

API设计

​编辑 快速排序

排序原理:

 API设计

 切分原理:

排序的稳定性

线性表

数组

数组例题1:找到数组中重复的数字

数组例题2:找到数组中消失的数字

数组例题3:多数元素

稀疏数组

双指针应用

链表

数组转链表:

链表例题1:链表中倒数第k个节点

链表例题2:反转链表

双链表

约瑟夫环问题:

队列

双端队列

队列例题1:队列最大值

队列例题2:滑动窗口最大值

循环队列

栈 Stack

栈例题1:有效的括号

栈的数组实现方式

栈例题2:队列实现栈

 栈例题3:栈实现队列 

 栈例题4:栈的最小值

 栈例题5:下一个更大元素

算数表达式:

栈例题:逆波兰表达式

 拓展:中缀转后缀表达式(代码为上)

散列表(哈希表)

什么是哈希表?

 Hash的应用

散列表的查找步骤 

哈希算法:

散列法:

散列冲突(哈希冲突)

哈希应用

哈希例题1:两数之和

哈希例题2:两数之和

递归

斐波那契数列(Fibonacci sequence)

汉诺塔问题(不能通过迭代处理)

阶乘

倒叙输出正整数

二叉树

满二叉树

 完全二叉树

​编辑 斜树

二叉树例题:树的深度

新建完全二叉树

完全二叉树 

递归实现遍历完全二叉树 

广度优先遍历 

非递归实现遍历完全二叉树 

例题1:对称二叉树

例题2:翻转二叉树

二叉查找树

堆的特性:

 API设计  

 堆排序

优先队列

 最大优先队列

 最小优先队列

 索引优先队列

平衡树

2-3查找树

红黑树

 平衡化

B-树

B树的特性

B树中插入数据 

B+树

 B+树存储数据

​编辑 B+树和B树对比

B+树在数据库中的应用

 并查集

 并查集结构

 API设计

并查集算法优化

 路径压缩

 案例:畅通工程

图 

基础知识

图的分类

无向图

 图的相关术语

图的存储结构

邻接矩阵

图的实现

图的搜索

路径查找

有向图

相关术语

有向图API设计

拓扑排序

检测有向图中的环

基于深度优先的顶点排序

拓扑排序实现

拓扑排序测试

加权无向图

加权无相图的表示

 加权无向图的实现

最小生成树

最小生成树原理

贪心算法

Prim算法(领接表)

 Prim算法(领接矩阵)

 krusskai算法

加权有向图

边的表示

加权有向图的实现

最短路径

定义及性质

松弛技术


     

        最近在学习数据结构与算法相关的知识,所以打算通过写笔记的方式加强自己对数据结构与算法的理解,也是为了方便以后复习。这里整理记录了一份数据结构与算法的思维导图,也是为了以后学习更有方向性。

介绍

数据结构&算法的定义

        数据结构就是指一组数据的存储结构。 算法就是操作数据的一组方法。 数据结构和算法,是指某些著名的队列、栈、堆、二分查找、动态规划等。

数据结构和算法的关系

        数据结构和算法是相辅相成的。
        数据结构是为算法服务的,算法要作用在特定的数据结构之上。因此,我们无法孤立数据结构来讲算法,也无法孤立算法来讲数据结构。比如,因为数组具有随机访问的特点,常用的二分查找算法需要用数组来存储数据。但如果我们选择链表这种数据结构,二分查找算法就无法工作了,因为链表并不支持随机访问。

        数据结构是静态的,它只是组织数据的一种方式。如果不在它的基础上操作、构建算法,孤立存在的数据结构就是没用的

数据结构与算法整体知识

20 个最常用的、最基础数据结构与算法:

10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树;

10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法。

思维导图

数据结构的分类

传统上,数据结构分为逻辑结构和物理结构两大类。

逻辑结构

        逻辑结构是从具体问题中抽象出来的模型,是抽象意义上的结构,按照对象中数据元素之间的相互分类,也是我们后面课题中需要关注和讨论的问题。

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

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

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

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

物理结构

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

顺序存储结构:

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

链式存储结构:

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

算法的分析

       有关算法时间耗费分析,我们称之为算法时间复杂度分析, 时间复杂度分析,有关算法空间耗费分析,我们称之为算法的空间复杂度分析。

  时间复杂度分析

事后估算法:算法执行完计算时间的方法,效率低。

事前估算法:程序编写前,根据统计方法对算法进行估算。时间消耗的因素:

        1.算法采用的策略和方案;

        2.编译产生的代码质量;

        3.问题的输入规模(输入量);

        4.机器执行指令的速度;

分析一个算法的运行时间,最重要的就是把核心操作的次数和输入规模关联起来。

大O记法

定义:

        在进行算法分析时,语句的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n的变化情况并确定T(n)的量级。算法的时间复杂度,就是算法的时间量度,记作:T(n)=O(f(n)).它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度,其中f(n)是问题规模tn的某个函数。执行次数=执行时间

        随着输入规模n的增大,T(n)增长最慢的算法为最优算法。此记法我们称为大O记法。

        

大O阶

线性阶 O(n)

平方阶 O(n^2)

立方阶 O(n^3)

 忽略底数

 常数阶O(1)

 

最坏情况 

 空间复杂度分析

      算法在运行过程中对内存的占用情况也是一个需要考虑的问题,我们用空间复杂度来描述算法对内存的占用。

常见内存占用基本数据类型

 算法的空间复杂度计算公式记作:S(n)=O(f(n));其中n为输入规模,f(n)为语句关于n所占存储空间的函数。

 

 排序算法

冒泡排序

需求:{6,5,4,3,2,1}->{1,2,3,4,5,6}

排序原理:

1.比较相邻的元素。如果前一个元素比后一个元素大,就交换这两个元素的位置。

2.对每一对相邻元素做相同的工作,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大值。

冒泡排序API设计

类名Bubble
构造方法Bubble(); 创建Bubble对象
成员方法

1.public static void sort(Comparable[] a); 对数组内的元素进行排序

2.private static boolean greater(Comparable v,Comparable w); 判断v是大于w

3.private static void exch(Comparable[] a,int i,int j); 交换a数组中,索引i和索引j处的值

public class Bubble {

    public static void main(String[] args) {
        Integer[] arr ={4,5,6,3,2,1};
        Bubble.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    //对数组内的元素进行排序
    public static void sort(Comparable[] a) {
        for (int i = a.length-1;i>0;i--){
            for (int j = 0; j < i; j++) {
                //比较索引j和索引j+1处的值
                if (greater(a[j],a[j+1])){
                    exch(a,j,j+1);
                }

            }
        }
    }

    // 判断v是大于w
    private static boolean greater(Comparable v, Comparable w) {
        return v.compareTo(w)>0;
    }

    //交换a数组中,索引i和索引j处的值
    private static void exch(Comparable[] a, int i, int j) {
        Comparable temp;
        temp = a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
[1, 2, 3, 4, 5, 6]

 冒泡排序时间复杂度为O(n^2)

选择排序 

需求:

排序前:{4,6,8,7,9,2,10,1}         排序后:{1,2,4,5,7,8,9,10}

排序原理:

        1.每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值一次进行比较,如果当前索引处的值大于其他某个索引处的值,则假定其他某个索引处的值为最小值,最后可以找到最小值所在的索引

        2.交换第一个索引处和最小值所在的索引处的值

API设计

类名Selection
构造方法Selection() ; 创建Selection对象
成员方法

1.public static void sort(Comparable[] a); 对数组内的元素进行排序

2.private static boolean greater(Comparable v,Comparable w); 判断v是大于w

3.private static void exch(Comparable[] a,int i,int j); 交换a数组中,索引i和索引j处的值

public class Select {


    public static void main(String[] args) {
        Integer[] arr ={4,6,8,7,9,2,10,1};
        Select.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    //对数组内的元素进行排序
    public static void sort(Comparable[] a) {
        for (int i = 0;i<= a.length-2;i++){
            //定义一个变量,记录最小元素所在的索引,默认为参与选择排序的第一个元素所在的位置
            int minIndex =i;
            for (int j = i+1; j < a.length; j++) {
                //比较最小索引minIndex和索引j处的值
                if (greater(a[minIndex],a[j])){
                    minIndex=j;
                }
            }
            //交换最小元素所在索引minIndex处的值和索引i处的值
            exch(a,i,minIndex);
        }
    }

    // 判断v是大于w
    private static boolean greater(Comparable v, Comparable w) {
        return v.compareTo(w)>0;
    }

    //交换a数组中,索引i和索引j处的值
    private static void exch(Comparable[] a, int i, int j) {
        Comparable temp;
        temp = a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
[1, 2, 4, 6, 7, 8, 9, 10]

 选择排序时间复杂度为O(n^2)

插入排序

插入排序是一种简单直观且稳定的排序算法。 

     

需求:

排序前:{4,3,2,10,12,1,5,6}         排序后:{1,2,3,4,5,6,10,12}

 排序原理:

1.把所有的元素分为两组,已经排序的和未排序的;

2.找到未排序的组中的第一个元素,向已经排序的组中进行插入;

3.倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素等于插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动一位;

 API设计

类名Insertion
构造方法Insertion() ; 创建Insertion对象
成员方法

1.public static void sort(Comparable[] a); 对数组内的元素进行排序

2.private static boolean greater(Comparable v,Comparable w); 判断v是大于w

3.private static void exch(Comparable[] a,int i,int j); 交换a数组中,索引i和索引j处的值

public class Insertion {


    public static void main(String[] args) {
        Integer[] arr ={4,3,2,10,12,1,5,6};
        Insertion.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    //对数组内的元素进行排序
    public static void sort(Comparable[] a) {
        for (int i = 1;i< a.length;i++){
          //比较索引j处的值和索引j-1处的值,如果索引j-1处的值比索引j处的值大
         // ,则交换数据,如果不大,那么就找到合适的位置了,退出循环即可
            for (int j = i; j >0; j--) {
                if (greater(a[j-1],a[j])){
                    exch(a,j-1,j);
                }else {
                    break;
                }
            }

        }
    }

    // 判断v是大于w
    private static boolean greater(Comparable v, Comparable w) {
        return v.compareTo(w)>0;
    }

    //交换a数组中,索引i和索引j处的值
    private static void exch(Comparable[] a, int i, int j) {
        Comparable temp;
        temp = a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
[1, 2, 3, 4, 5, 6, 10, 12]

  插入排序时间复杂度为O(n^2)


希尔排序 

希尔排序是插入排序的一种,又称“缩小增量排序”,是插入排序算法的一种更高效地改进版本

需求:

        排序前:{9,1,2,5,7,4,8,6,3,5}        排序后:{1,2,3,4,5,5,6,7,8,9}

排序原理

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

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

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

 希尔排序的API设计

类名Shell
构造方法Shell(); 构造对象
成员方法

1.public static void sort(Comparable[] a) 对数组内的元素进行排序

2.private static boolean greater(Comparable v,Comparable w) 判断v是否大于w

3.private static void exch(Comparable[] a,int i,int j)  交换a数组中,索引i和索引j处的值

public class Shell {

    public static void main(String[] args) {
        Integer[] arr ={9,1,2,5,7,4,8,6,3,5};
        Shell.sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    //对数组内的元素进行排序
    public static void sort(Comparable[] a) {
        //1.根据数组a的长度,确定增长量h的初始值;
        int h=1;
        while(h<a.length/2){
            h=2*h+1;
        }
        //2.希尔排序
        while (h>=1){
            //排序
            // 2.1找到待插入的元素
            for (int i=h; i< a.length;i++){
                //2.2把待插入的元素插入到有序数列中
                for (int j=i;j>=h;j-=h){
                    //待插入的元素是a[j].比较a[j]和a[j-h]
                    if (greater(a[j-h],a[j])){
                        //交换元素
                        exch(a,j-h,j);
                    }else {
                        //待插入元素已经找到了合适的位置,结束循环;
                        break;
                    }

                }
            }
            //减少h的值
            h=h/2;
        }


    }


    // 判断v是大于w
    private static boolean greater(Comparable v, Comparable w) {
        return v.compareTo(w)>0;
    }

    //交换a数组中,索引i和索引j处的值
    private static void exch(Comparable[] a, int i, int j) {
        Comparable temp;
        temp = a[i];
        a[i]=a[j];
        a[j]=temp;
    }
}
[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]

 事后分析希尔排序执行时间较插入排序短

归并排序 

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列间断有序。若将两个有序表合并成一个有序表,称为二路归并。

需求:

排序前:{8,4,5,7,1,3,6,2}        排序后:{1,2,3,4,5,6,7,8}

排序原理:

1.尽可能的一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止。

2.将相邻的两个子组进行合并成一个有序的大组;

3.不断的重复步骤2,直到最终只有一个组为止。

API设计

类名Merge
构造方法Merge()  创建Merge对象
成员方法

1.public static void sort(Comparable[] a)   对数组内的元素进行排序

2.private static void sort(Cmparable[] a,int lo,int hi) 对数组a中从索引lo到索引hi之间的元素进行排序

3.private static void merge(Comparable[] a,int lo,int mid, int hi); 从索引lo到索引mid为一个子组,从索引mid+1到索引hi为另一个子组,把数组a中的这两个子组的数据合并成一个有序的大组(从索引lo到索引hi)

4.private static boolean less(Comparable v,Comparable w);  判断v是否小于w

5.private static void exch(Comparable[] a,int i,int j );交换a数组中,索引i和索引j处的值

成员变量1.private static Comparable[] assist ; 完成归并操作需要的辅助数组

public class Merge {

    public static void main(String[] args) {
        Integer[] arr ={9,1,2,5,7,4,8,6,3,5};
        Merge.sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    private static Comparable[] assist; //完成归并操作需要的辅助数组

    // 对数组内的元素进行排序
    public static void sort(Comparable[] a) {
        //1.初始化辅助数据assit;
        assist = new Comparable[a.length];
        //2.定义一个lo遍历,和hi变量,分别记录数组中最小的索引和最大的索引;
        int lo = 0;
        int hi = a.length-1;
        //3.调用sort重载方法完成数据a中,从索引lo到索引hi的元素的排序
        sort(a,lo,hi);
    }

    //对数组a中从索引lo到索引hi之间的元素进行排序
    private static  void sort(Comparable[] a, int lo, int hi) {
        //做安全性校验
        if (hi<=lo){
            return;
        }
       //对lo到hi之间的数据进行分为两个组
       int mid = lo+(hi-lo)/2; //5,9 mid=7

       //分别对每一组数据进行排序
       sort(a,lo,mid);
       sort(a,mid+1,hi);
       //再把两个组中的数据进行归并
        merge(a,lo,mid,hi);
    }

    //从索引lo到索引mid为一个子组,从索引mid+1到索引hi为另一个子组
//,把数组a中的这两个子组的数据合并成一个有序的大组(从索引lo到索引hi)
    private static void merge(Comparable[] a, int lo, int mid, int hi) {
        //定义三个指针
        int i=lo;
        int p1 = lo;
        int p2 = mid+1;

        //遍历,移动p1指针和p2指针,比较对应索引处的值,找出小的那个,放到辅助数组的对应索引处
        while(p1<=mid&&p2<=hi){
            //比较对应索引处的值
            if(less(a[p1],a[p2])){
                assist[i++]=a[p1++];
            }else {
                assist[i++]=a[p2++];
            }
        }
        //遍历,如果p1的指针没有走完,那么顺序移动p1指针,把对应的元素放到辅助数组的对应索引处
        while(p1<=mid){
            assist[i++]=a[p1++];
        }
        //遍历,如果p2的指针没有走完,那么顺序移动p2指针,把对应的元素放到辅助数组的对应索引处
        while(p2<=hi){
            assist[i++]=a[p2++];
        }

        //把辅助数组中的元素拷贝到原数组中
        for (int index = lo; index <=hi; index++) {
            a[index]=assist[index];

        }

    }

    // 判断v是否小于w
    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w)<0;
    }

    //  交换a数组中索引i和索引j处的值
    private static void exch(Comparable[] a, int i, int j) {
        Comparable t = a[i];
        a[i]=a[j];
        a[j]=t;
    }
}
[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]

 快速排序

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

需求:

排序前:{6,1,2,7,9,3,4,5,8}        排序后:{1,2,3,4,5,6,7,8,9}

排序原理:

1.首先设定一个分界值,通过该分界值将数组分成左右两部分;

2.将大于或等于分界值的数据放到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,

而右边部分中各元素都大于等于分界值;

3.然后,左边和右边的数据都可以独立排序。对于左侧的数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。

 API设计

类名Quick
构造方法Quick() ;创建Quick对象
成员方法 1.public static void sort(Comparable[] a)   对数组内的元素进行排序

2.private static void sort(Cmparable[] a,int lo,int hi) 对数组a中从索引lo到索引hi之间的元素进行排序

3.private static int partition(Comparable[] a,int lo,int mid, int hi);对数组a中, 从索引lo到索引hi之间进行分组,并返回分组界限对应的索引

4.private static boolean less(Comparable v,Comparable w);  判断v是否小于w

5.private static void exch(Comparable[] a,int i,int j );交换a数组中,索引i和索引j处的值

 切分原理:

把一个数组切分成两个子数组的基本思想:

1.找一个基准值,用两个指针分别指向数组的头部和尾部;

2.先从尾部向头部开始搜索一个比基准值小的元素,搜索到即停止,并记录指针的位置;

3.先从尾部向头部开始搜索一个比基准值小的元素,搜索到即停止,并记录指针的位置;

4.交换当前左边指针位置和右边指针位置的元素;

5.重复2,3,4步骤,直到左边指针的值大于右边指针的值停止。

 

public class Quick {

    public static void main(String[] args) {
        Integer[] arr ={9,1,2,5,7,4,8,6,3,5};
        Quick.sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    private static Comparable[] assist; //完成归并操作需要的辅助数组

    // 对数组内的元素进行排序
    public static void sort(Comparable[] a) {
        //1.初始化辅助数据assit;
        assist = new Comparable[a.length];
        //2.定义一个lo遍历,和hi变量,分别记录数组中最小的索引和最大的索引;
        int lo = 0;
        int hi = a.length-1;
        //3.调用sort重载方法完成数据a中,从索引lo到索引hi的元素的排序
        sort(a,lo,hi);
    }

    //对数组a中从索引lo到索引hi之间的元素进行排序
    private static  void sort(Comparable[] a, int lo, int hi) {
        //做安全性校验
        if (hi<=lo){
            return;
        }
       //需要对数组中lo索引到hi索引处的元素进行分组(左子组和右子组);
       int partition = partition(a,lo,hi); //返回的是分组的分界值所在的索引,分界值位置变换后的索引

        //让左子组有序
        sort(a, lo, partition-1);

        //让右子组有序
        sort(a, partition+1, hi);
    }
    //对数组a中,从索引lo到索引hi之间进行分组,并返回分组界限对应的索引
    private static int partition(Comparable[] a,int lo, int hi){
        //确定分界值
        Comparable key = a[lo];
        //定义两个指针,分别指向待切分的元素的最小索引处和最大索引处的下一个位置
        int left = lo;
        int right = hi+1;

        //切分
        while(true){
            //先从右往左扫描,移动right指针,找到一个比分界值小的元素,停止
            while(less(key,a[--right])){
                if(right==lo){
                    break;
                }
            }
            //再从左往右扫描,移动left指针,找到一个比分界值大的元素,停止
            while(less(a[++left],key)){
                if (left==hi){
                    break;
                }
            }
            //判断left>=right,如果是,则证明元素扫描完毕,结束循环,如果不是,则交换元素即可
            if (left>=right){
                break;
            }else {
                exch(a,left,right);
            }
        }
        //交换分界值
        exch(a,lo,right);
        return right;

    }

    // 判断v是否小于w
    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w)<0;
    }

    //  交换a数组中索引i和索引j处的值
    private static void exch(Comparable[] a, int i, int j) {
        Comparable t = a[i];
        a[i]=a[j];
        a[j]=t;
    }
}
[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]

 

 最坏情况,快速排序时间复杂度为O(n^2)

排序的稳定性

线性表

        线性表(Linear List),就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向,包括数组,链表、队列、栈等。

数组

        数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的变量。

数组例题1:找到数组中重复的数字

数组中重复的数字
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3 
限制:

2 <= n <= 100000

/**
 * 查找数组中重复的数字
 * 输入:
 * [2, 3, 1, 0, 2, 5, 3]
 * 输出:2 或 3
 */
public class FindRepeatNumber {
    //测试方法
    public static void main(String[] args) {
        int[] nums = {2, 3, 1, 0, 2, 5, 3};
//        System.out.println(findRepeatNumber1(nums));
//        System.out.println("---------------------");
//        System.out.println(findRepeatNumber2(nums));
//        System.out.println("---------------------");
//        System.out.println(findRepeatNumber3(nums));
//        System.out.println("---------------------");
        System.out.println(findRepeatNumber4(nums));

    }

    //1.集合的解法 Set
    public static int findRepeatNumber1(int[] nums) {
        Set<Integer> set = new HashSet<>();
        //遍历数组
        for (int num : nums) {
            if (set.contains(num)) {
                System.out.println("发现重复元素" + num);
                return num;
            }
            System.out.println("添加元素" + num);
            set.add(num);
        }

        return -1;
    }

    //2.先排序,再查找
    public static int findRepeatNumber2(int[] nums) {
        //数组工具类arrays排序和打印数组
        Arrays.sort(nums);
        System.out.println(Arrays.toString(nums));
        //itar联想for循环 判断相邻元素是否相同
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] == nums[i - 1]) {
                System.out.println("发现重复元素" + nums[i]);
                return nums[i];
            }
        }

        return -1;
    }

    //3.使用临时数组记录出现的数字
    public static int findRepeatNumber3(int[] nums) {
        //临时数组,所有元素默认为0
        int[] temp = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            //判断再次赋值是否为1,为1则已经有元素,后来元素重复
            if (temp[num] == 1) {
                System.out.println("发现重复元素" + num);
                return num;
            }
            //找到临时数组索引位置,赋值为1
            temp[num] = 1;

        }

        return -1;
    }

    //使用自身数组判断重复元素  让数字出现在索引位置上,如果交换完遍历数已经出现,则为重复元素
    public static int findRepeatNumber4(int[] nums) {
        //交换前打印
        System.out.println(Arrays.toString(nums));
        //遍历
        for (int i = 0; i < nums.length; i++) {
            //如果索引正好等于元素本身,是期望的结果  跳过
            if(nums[i]==i) continue;
            //i=0,nums[0]=2,nums[2]=1;
            int num =nums[i];//记录数组当前位置的值
            int temp =nums[num];//记录值为索引位置的值
            //交换的位置已经有期望值,则重复了
            if (nums[num]==nums[i]) {
                System.out.println("当前索引为"+i+"的位置上已经与索引为"+num+"的位置上元素重复");
                return num;
            }
            //交换顺序
            nums[num]=num;//值为索引位置的值置为与索引相同
            nums[i]=temp;//将原索引位置的值放入当前位置

            //交换仍需遍历当前位置的值,之后i++抵消
            i--;
            //交换后打印
            System.out.println(Arrays.toString(nums));


        }
        return -1;
    }
}
执行结果:
法1:
添加元素2
添加元素3
添加元素1
添加元素0
发现重复元素2
2
法2:
[0, 1, 2, 2, 3, 3, 5]
发现重复元素2
2
法3:
发现重复元素2
2
法4:
[2, 3, 1, 0, 2, 5, 3]
[1, 3, 2, 0, 2, 5, 3]
[3, 1, 2, 0, 2, 5, 3]
[0, 1, 2, 3, 2, 5, 3]
当前索引为4的位置上已经与索引为2的位置上元素重复
2

数组例题2:找到数组中消失的数字

找到所有数组中消失的数字

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

示例 1:

输入:nums = [4,3,2,7,8,2,3,1]
输出:[5,6]
/**
 * 消失的数字
 * 给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出
 * 所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
 * 输入:nums = [4,3,2,7,8,2,3,1]
 * 输出:[5,6]
 */
public class FindDisappearedNumber {
    public static void main(String[] args) {
        int[] nums =  {4,3,2,7,8,2,3,1};
        System.out.println(findDisappearedNumbers(nums));
        System.out.println(findDisappearedNumbers1(nums));
    }

    //Set集合,不重复且无序
    //List,   可能重复且有序
    //map,     <元素,出现的次数> 映射关系
    public static List<Integer> findDisappearedNumbers(int[] nums) {
        List<Integer> result =new ArrayList<>();
        //使用Set 存储已出现的数字,去重
        Set<Integer> set =new HashSet<>();
        for (int num:nums){
            set.add(num);
        }
        //1-n开始遍历,使用List记录set未出现的元素
        for (int i = 1; i < nums.length; i++) {
            if(!set.contains(i)){
                result.add(i);
            }
            
        }
        
        return result;
    }
    //进阶:不使用额外空间
    //利用索引记录已出现的数字
    //元素出现时,将其对应索引位置的元素置为负数,当剩余场上还有正数时,说明其对应索引位置的值未出现
    public static List<Integer> findDisappearedNumbers1(int[] nums) {
        List<Integer> result =new ArrayList<>();
        for (int i = 0; i < nums.length; i++) {//nums = [4,3,2,7,8,2,3,1]
            int num = Math.abs(nums[i]);
            int index=num-1;
            //将对应索引位置的值置为负数,前提是这个数为正数
            if(nums[index]>0){
                nums[index]=-nums[index];
                System.out.println("索引"+index+ Arrays.toString(nums));
            }
        }
        //再次遍历,找到消失的元素
        for (int i = 0; i < nums.length; i++) {
            if (nums[i]>0){
                result.add(i+1);
                System.out.println(nums[i]+"元素仍为正数,找到其下标"+(i+1));
            }
            
        }
        
        return result;
    }
}
执行结果:
[5, 6]
---------------------------
索引3[4, 3, 2, -7, 8, 2, 3, 1]
索引2[4, 3, -2, -7, 8, 2, 3, 1]
索引1[4, -3, -2, -7, 8, 2, 3, 1]
索引6[4, -3, -2, -7, 8, 2, -3, 1]
索引7[4, -3, -2, -7, 8, 2, -3, -1]
索引0[-4, -3, -2, -7, 8, 2, -3, -1]
8元素仍为正数,找到其下标5
2元素仍为正数,找到其下标6
[5, 6]

数组例题3:多数元素

多数元素

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入:nums = [3,2,3]
输出:3
示例 2:

输入:nums = [2,2,1,1,1,2,2]
输出:2

提示:
n == nums.length
1 <= n <= 5 * 104
-109 <= nums[i] <= 109

/**
 * 多数元素
 * 给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于[ n/2 ]的元素。
 * 你可以假设数组是非空的,并且给定的数组总是存在多数元素。
 * 输入:nums = [3,2,3]
 * 输出:3
 */
public class MajorityNumber {
    public static void main(String[] args) {
       // int[] nums ={3,2,3};
        int[] nums ={ 2,2,1,1,1,2,2};
        //System.out.println(majorityElement1(nums));
        System.out.println("-------------------");
       // System.out.println(majorityElement2(nums));
        System.out.println("-------------------");
        System.out.println(majorityElement3(nums));
    }

    //法1:map<元素,出现的次数>  如果次数超过n/2,则为多数元素
    public static int majorityElement1(int[] nums) {
        Map<Integer,Integer> map = new HashMap<>();
        int halflen =nums.length/2;
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            int count =1;
            //如果map已经出现该元素,将count值取出并自增
            if (map.containsKey(num)){
                count=map.get(num);
                count++;
            }
            //判断是否是多数元素
            if (count>halflen) return num;

            System.out.println(num+"元素存储次数为"+count);
            map.put(num,count);
            
        }
        return -1;
    }

    //法2:数组排序,取中间的元素,为多数元素
    public static int majorityElement2(int[] nums) {
        Arrays.sort(nums);
        System.out.println(Arrays.toString(nums));
        int halflen =nums.length/2;
        return nums[halflen];
    }

    //法3:投票算法
    //元素当候选人,新元素和候选人相同,次数加1,不同,次数减一(抵消)
    //若次数为0,则候选人更新为新元素,最终为正数的为多数元素
    public static int majorityElement3(int[] nums) {
        //初始化候选人
        int candidate = -1;
        //定义次数
        int count =0;
        for (int i = 0; i < nums.length; i++) {

            //如果出现次数为0,重置候选人
            if (count==0){
                candidate=nums[i];
            }

            //根据新元素加减候选人
          if (candidate==nums[i]){
              count++;
          }else {
              count--;
          }
            System.out.println("元素"+nums[i]+",次数"+count+",候选人"+candidate);
        }
        return candidate;
    }
}
执行结果:
法1:
2元素存储次数为1
2元素存储次数为2
1元素存储次数为1
1元素存储次数为2
1元素存储次数为3
2元素存储次数为3
2
法2:
[1, 1, 1, 2, 2, 2, 2]
2:
法3:
元素2,次数1,候选人2
元素2,次数2,候选人2
元素1,次数1,候选人2
元素1,次数0,候选人2
元素1,次数1,候选人1
元素2,次数0,候选人1
元素2,次数1,候选人2
2

稀疏数组

        一个数组中大部分元素为0,或者为同一个值时,可以使用稀疏数组来保存该数组,进行数组内容的压缩,避免记录大量无意义的值。

  稀疏数组的处理方法是:

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

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

//稀疏数组
public class SparseArray {

    public static void main(String[] args) {
        int[][] arr =new int[10][10];
        arr[1][2]=1;
        arr[3][1]=2;
        arr[2][2]=2;
        System.out.println("输出原始数组:");
        for (int[] ints : arr) {
            for (int anInt : ints) {
                System.out.print(anInt+"\t");
            }
            System.out.println();
        }
        System.out.println("输出稀疏数组");
        int[][] result =toSparse(arr);
        for (int i = 0; i < result.length; i++) {
            System.out.println(result[i][0]+"\t"+result[i][1]+"\t"+result[i][2]);

        }
        System.out.println("-------------------------");
        int[][] arr1=sparseTOArray(result);
        System.out.println("输出再转回来的数组:");
        for (int[] ints : arr) {
            for (int anInt : ints) {
                System.out.print(anInt+"\t");
            }
            System.out.println();
        }
    }

    //把普通数组 转化为稀疏数组
    public static int[][] toSparse(int[][] arr){

        //(n+1)*3列的数组 初始化
        //需要先求非零元素的个数
        int count =0;
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr[0].length; j++) {
                if (arr[i][j]!=0){
                    count++;
                }
            }
        }
        //定义稀疏数组,行数取决于元素个数,d第一行记录原数组总行数和总列数和元素个数
        int[][] result = new int[count+1][3];
        result[0][0]=arr.length;
        result[0][1]=arr[0].length;
        result[0][2]=count;

        //遍历数组  找到所有非0元素,存到结果中
        int index =0;//定义稀疏行下标
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length; j++) {
               if(arr[i][j]!=0){
                   index++;
                   result[index][0]=i;
                   result[index][1]=j;
                   result[index][2]=arr[i][j];

               }

            }
            
        }
        return result;
    }

    //将稀疏数组转化为普通数组,默认稀疏数组为3列
    public static int[][] sparseTOArray(int[][] result){
        int i=result[0][0];
        int j=result[0][1];
        int[][] arr = new int[i][j];
        for (int index = 1; index < result.length; index++) {
            arr[result[index][0]][result[index][1]]=result[index][2];
        }
        return arr;
    }
}
执行结果:
输出原始数组:
0	0	0	0	0	0	0	0	0	0	
0	0	1	0	0	0	0	0	0	0	
0	0	2	0	0	0	0	0	0	0	
0	2	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
输出稀疏数组
10	10	3
1	2	1
2	2	2
3	1	2
-------------------------
输出再转回来的数组:
0	0	0	0	0	0	0	0	0	0	
0	0	1	0	0	0	0	0	0	0	
0	0	2	0	0	0	0	0	0	0	
0	2	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	
0	0	0	0	0	0	0	0	0	0	

双指针应用

        数组的遍历过程索引就是一个指针的一个引用

        双指针有不同的遍历方式

        对撞指针:两指针相反方向遍历

        快慢指针:两指针相同方向,遍历速度不同,或起点不同

应用1:

删除有序数组中的重复项

给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。

由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 k 个元素,那么 nums 的前 k 个元素应该保存最终结果。

将最终结果插入 nums 的前 k 个位置后返回 k 。

不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例 1:

输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:

输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

/**
 * 快慢指针应用
 * 删除重复元素,不使用新空间
 * 输入:nums = [0,0,1,1,1,2,2,3,3,4]
 * 输出:5, nums = [0,1,2,3,4]
 * 解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改
 * 为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
 */
public class DeleteDuplicate {
    public static void main(String[] args) {
        int[] nums = {0,0,1,1,1,2,2,3,3,4};
       int n=removeDuplicates(nums);
        int[] ints = Arrays.copyOfRange(nums, 0, n);
        System.out.println("删除重复元素后数组为:");
        System.out.println(Arrays.toString(ints));

    }

    //双指针解法,第一个指针负责遍历并与第一个指针比较
    //第二个指针记录重复位置并覆盖
    public static int removeDuplicates(int[] nums) {
        if (nums.length==0) return 0;
        //用于比较元素的位置
        int index =0;
        for (int i = 1; i < nums.length; i++) {
            //如果遇到不同  与index+1进行覆盖
            if(nums[i]!=nums[index]){
                index++;
                nums[index]=nums[i];
                System.out.println("覆盖第"+index+"位置的元素为"+nums[i]);
            }
        }
        return index+1;
    }
}
执行结果:
覆盖第1位置的元素为1
覆盖第2位置的元素为2
覆盖第3位置的元素为3
覆盖第4位置的元素为4
删除重复元素后数组为:
[0, 1, 2, 3, 4]

应用2:

移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

/**
 *
 * 输入:nums = [0,1,2,2,3,0,4,2], val = 2
 * 输出:5, nums = [0,1,4,0,3]
 * 解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
 * 注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
 */
public class DeleteElement {

    public static void main(String[] args) {
        int[] nums ={0,1,2,2,3,0,4,2};
//        int n =removeElement(nums,2);
//        System.out.println(Arrays.toString(nums));
//        int[] ints = Arrays.copyOfRange(nums, 0, n);
//        System.out.println(Arrays.toString(ints));
        int m =removeElement1(nums,2);
      
        int[] ints1 = Arrays.copyOfRange(nums, 0, m);
        System.out.println(Arrays.toString(ints1));
    }
    //法1:双指针覆盖法
    public static int removeElement(int[] nums, int val) {
        if (nums.length==0) return 0;
        //用于比较元素的位置
        int index =0;
        for (int i = 0; i < nums.length; i++) {
            //如果值不为val  与index进行覆盖
            if(nums[i]!=val){
                //覆盖当前位置
                nums[index]=nums[i];//0,1,2,2,3,0,4,2
                System.out.println("覆盖第"+index+"位置的元素为"+nums[i]);
                index++;

            }
        }
        return index;
    }
    //法2:指针末位数替换法(对撞指针)
    //判断遍历当前值是否为要删除值,是就和倒数位元素替换,倒数位元素自减
    public static int removeElement1(int[] nums, int val) {
        //初始化两指针
        int index =0;
        int len =nums.length;//记录有效元素个数
        while(true){
            if (index>=len) return len;

            if (nums[index]==val){
                nums[index]=nums[len-1];
                 nums[len-1]=val;

                System.out.println("替换位置"+index+"和"+(len-1)+"的元素");
                System.out.println(Arrays.toString(nums));

                len--;
                //抵消再判断,防止替换后的值仍为val;
                index--;
            }
            index++;
        }

    }
}
执行结果:
覆盖第0位置的元素为0
覆盖第1位置的元素为1
覆盖第2位置的元素为3
覆盖第3位置的元素为0
覆盖第4位置的元素为4
[0, 1, 3, 0, 4, 0, 4, 2]
[0, 1, 3, 0, 4]

法2:
替换位置2和7的元素
[0, 1, 2, 2, 3, 0, 4, 2]
替换位置2和6的元素
[0, 1, 4, 2, 3, 0, 2, 2]
替换位置3和5的元素
[0, 1, 4, 0, 3, 2, 2, 2]
[0, 1, 4, 0, 3]

链表

        链表包含单链表,双向链表,循环链表等等。相对于线性表,添加,删除操作非常方便,因为不用移动大量的节点,只需要修改对应的前后节点指针即可。下面用一个具体实例来说明下这种结构。现在有一需求,是将具有不同编号,姓名,昵称的人添加到系统中。首先需要创建节点,既然是链表,节点除了基本信息也要加入下一节点指针,方便计算机在内存中查找。

链表分为有头节点的链表和没有头节点的链表。

插入的时候,分为头插法和尾插法。

节点关系称之为前置节点和后继节点。

节点的构成,包含数据本身,以及对后继节点的引用。

与数组区别:

优点:链表实现了真正的动态,不需要处理固定容量带来的系列问题

             增删快

缺点:失去随机访问能力,查询慢

数组转链表:

public class ListNode1 {

    public static void main(String[] args) {
        String[] arr ={"王者荣耀","和平精英","开心消消乐","欢乐斗地主"};
        ListNode1 node = ListNode1.arrayToListNode(arr);
        System.out.println();
    }

     String data;
    ListNode1 next;
    public ListNode1(String data){
        this.data=data;
    }
    public static ListNode1 arrayToListNode(String[] arr){
        if(arr.length==0) return null;
        //生成链表的根节点
        ListNode1 root = new ListNode1(arr[0]);
        //生成临时节点
        ListNode1 pre =root;
        for (int i=1;i<arr.length;i++){
            ListNode1 node = new ListNode1(arr[i]);
            //创建链接关系,将前一个节点的next设置为当前节点
            pre.next=node;
            //更新pre为当前节点,下一个要处理的节点
            pre=node;
        }
        return root;
    }

}

链表例题1:链表中倒数第k个节点

链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:

给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

public class ListNode {
     int val;
    ListNode next;
    public ListNode(int val){
        this.val=val;
    }
    public static ListNode arrayToListNode(int[] arr){
        if(arr.length==0) return null;
        //生成链表的根节点
        ListNode root = new ListNode(arr[0]);
        //生成临时节点
        ListNode pre =root;
        for (int i=1;i<arr.length;i++){
            ListNode node = new ListNode(arr[i]);
            //创建链接关系,将前一个节点的next设置为当前节点
            pre.next=node;
            //更新pre为当前节点,下一个要处理的节点
            pre=node;
        }
        return root;
    }
    //pos代表尾结点指向链表的某个节点的索引位置(环的入口)
    public static void toCycle(ListNode node,int pos){
        //遍历  通过pos找到 入口对应的节点  记录下来
        //遍历到尾结点时,设置其为next引用
        int cnt=0;
        ListNode cycleNode =null;
        while(true){
            //判断是否是环的入口节点
            if(cnt==pos){
                cycleNode = node;
            }
            //判断是否是尾结点
            if(node.next==null){
               node.next= cycleNode;
               return;
            }
            node=node.next;
            cnt++;
        }
    }
}

/**
 * 输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
 *
 * 例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

 * 从尾部查找的第k个节点
 */
public class KthNodeFromEnd {
    public static void main(String[] args) {
        int[] arr ={1,2,3,4,5,6};
        ListNode head =ListNode.arrayToListNode(arr);
        ListNode node =getKthFromEnd(head,3);
        System.out.println(node.val);
        ListNode node1 =getKthFromEnd1(head,3);
        System.out.println(node1.val);
    }
    //法一
    public static ListNode getKthFromEnd(ListNode head, int k) {

        //先统计总共多少节点
        int n =0;
        ListNode temp=head;
      while(temp.next!=null){
          n++;
          temp=temp.next;//下移一个节点
      }

      //再次遍历,找到第n-k+1个节点,即为倒数第k个节点
        temp=head;
        for (int i = 0; i < n-k+1; i++) {
            temp=temp.next;
        }
        return  temp;
    }
    /**
     * 法二:双指针
     * 先定义快指针,先走k步,再定义慢指针,两指针同时走,当快指针走到尾部时,慢指针就找到倒数第k个节点
     */

    public static ListNode getKthFromEnd1(ListNode head, int k) {
        ListNode slow =head;
        ListNode fast=head;
        for (int i = 0; i <k-1; i++) {
            fast=fast.next;
        }
        while (fast.next!=null){
            fast=fast.next;
            slow=slow.next;
            System.out.println("slow移动到"+slow.val+",fast移动到"+fast.val);
        }

        return slow;
    }
}
执行结果:
4->5
----------------
slow移动到2,fast移动到4
slow移动到3,fast移动到5
slow移动到4,fast移动到6
4->5

链表例题2:反转链表

反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

/**
 * 定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
 * 示例:
 * 输入: 1->2->3->4->5->NULL
 * 输出: 5->4->3->2->1->NULL
 */
public class ReverseList {
    public static void main(String[] args) {
        int[] arr ={1,2,3,4,5};
        ListNode head =ListNode.arrayToListNode(arr);
        ListNode reasult = reverseList(head);
        print(reasult);
    }

    public static ListNode reverseList(ListNode head) {
        //记录前置节点和当前节点
        ListNode pre =null;
        ListNode cur=head;
        //不断移动cur节点,向后遍历,同时更改其next
        //当cur为null时,停止遍历
         while (cur!=null){
             //创建临时节点记录下个节点
             ListNode temp =cur.next;
             //更改指向
             cur.next=pre;
             if(cur!=null&&pre!=null){
                 System.out.println("让"+cur.val+"的next指向"+pre.val);
             }
             //pre和cur向后移动
             pre=cur;
             cur=temp;
         }
        return pre;
    }
    //打印链表
    public static void print(ListNode head){

        while (head!=null){
            System.out.print(head.val);
            if (head.next!=null){
                System.out.print("->");
            }
            head=head.next;
        }
    }
}
执行结果:
让2的next指向1
让3的next指向2
让4的next指向3
让5的next指向4
5->4->3->2->1

双链表

数据+下一个节点的引用+对上一个节点的引用(即数据加前置指针和后置指针)

链表例题3:环形链表

环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

链表例题4:环形链表II

环形链表 II

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
public class CycleList {
    public static void main(String[] args) {
        int[] arr ={3,2,0,-4};
        ListNode node =ListNode.arrayToListNode(arr);
        ListNode.toCycle(node,1);
        System.out.println(hasCyale(node));
       // System.out.println(detectCycle(node).val);
    }
    //判断链表是否有环
    public static boolean hasCyale(ListNode head){
        //链表不能为空
        if(head==null||head.next==null) return false;
        ListNode slow =head;
        ListNode fast =head;
        while (true) {
            if (fast==null||fast.next==null){
                return false;
            }
            slow=slow.next;
            fast=fast.next.next;
            if (slow!=null){
                System.out.println("slow走到"+slow.val);
            }
            if (fast!=null){
                System.out.println("fast走到"+fast.val);
            }
            //当快节点追上慢节点时,说明有环
            if (slow==fast) return true;
        }


    }
//
    public static ListNode detectCycle(ListNode head) {
        //链表不能为空
        if(head==null||head.next==null) return null;
        ListNode slow =head;
        ListNode fast =head;
        while (true) {
            if (fast==null||fast.next==null){
                return null;
            }
            slow=slow.next;
            fast=fast.next.next;
            if (slow!=null){
                System.out.println("slow走到"+slow.val);
            }
            if (fast!=null){
                System.out.println("fast走到"+fast.val);
            }
            //当快节点追上慢节点时,说明有环
            if (slow==fast) break;
        }
        //第一次相遇后,让快指针重新指向头节点,慢指针不变
        //fast和slow按照相同的速度移动,再次相遇的节点为环形节点的入口
        fast=head;
        //如果环节点为完全闭环那么头节点就为入口节点
        while (slow!=fast){
            slow=slow.next;
            fast=fast.next;
            if (slow!=null){
                System.out.println("slow走到"+slow.val);
            }
            if (fast!=null){
                System.out.println("fast走到"+fast.val);
            }
           
        }
             return slow;

    }
    
}
执行结果:
slow走到2
fast走到0
slow走到0
fast走到2
slow走到-4
fast走到-4
true
slow走到2
fast走到0
slow走到0
fast走到2
slow走到-4
fast走到-4
slow走到2
fast走到2
2

约瑟夫环问题:

约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3。

分析:

(1)由于对于每个人只有死和活两种状态,因此可以用布尔型数组标记每个人的状态,可用true表示死,false表示活。

(2)开始时每个人都是活的,所以数组初值全部赋为false。

(3)模拟杀人过程,直到所有人都被杀死为止。

public class Josephus {

    public static void main(String[] args) {
        //System.out.println("41,3 ->"+josephus(41,3));
        System.out.println("41,3 ->"+josephus1(41,3));
    }
    /**
     * 约瑟夫环问题-环形列表法
     * @param n  总人数
     * @param m   循环值
     * @return
     */
    public static int josephus(int n,int m){
        //初始化环形列表
        int[] arr = new int[n];
        for (int i = 0; i < arr.length; i++) {
             arr[i]=i+1;
        }
        ListNode node =ListNode.arrayToListNode(arr);
        ListNode.toCycle(node,0);

        //将每一个人视作链表中的一个节点,当报数到m的时候,删除节点,直到剩下最后一个节点
        //当找到报数m-1的节点 node node.next=node.next.next
        //当剩最后一个节点时,node.next=node
        int cnt =1;
        while (true){
            if(cnt==m-1){
                System.out.println("删除节点"+node.next.val);
                node.next=node.next.next;
                cnt=0;
            }
            cnt++;
            node=node.next;
            if (node.next==node) return node.val;
        }

    }

    /**
     * 约瑟夫环问题-数组法
     * @param n 总人数
     * @param m 报数值
     * @return
     */
    public static int josephus1(int n,int m){
        //数组记录,初始值为0,挂掉的人为-1
        int[] person =new int[n];

        //人的索引
        int index=-1;
        //报数记录 1 2 ... m
        int cnt =0;
        //剩余人数
        int remain =n;

        while (remain>0){

            index++;

            //到达数组尾部,重头遍历
            if (index>=n) index=0;

            //如果此人已经为-1,跳过
            if(person[index]==-1){
                continue;
            }
            //报数到m,将index对应位置人,置为-1
            if(cnt==m){
                person[index]=-1;
                cnt=0;
                remain--;
            }
            cnt++;

        }
        return index;
    }
}
执行结果:
41,3 ->31

队列

像栈一样,队列(queue)也是一种线性表,它的特性是先进先出,插入在一端,删除在另一端。就像排队一样,刚来的人入队(push)要排在队尾(rear),每次出队(pop)的都是队首(front)的人。如图1,描述了一个队列模型。

队列(Queue)与栈一样,是一种线性存储结构,它具有如下特点:

队列中的数据元素遵循“先进先出”(First In First Out)的原则,简称FIFO结构。
在队尾添加元素,在队头删除元素。
 

/**
 * 数组实现队列方式
 */
public class ArrayQueue {
    //最大容量
    int maxCapacity;

    //声明头尾指针
    int head;
    int tail;
    //存储元素
    int[] arr;

        //初始化
    public ArrayQueue(int maxCapacity){
        arr =new int[maxCapacity];
        this.maxCapacity=maxCapacity;
        head=0;
        tail=0;
    }
    public boolean isFull(){
        //尾指针到头时
        return tail==maxCapacity;
    }
    public boolean isEmpty(){
        return head==tail;
    }
    public void add(int n){
        //先判断队列容量大小
        if(!isFull()){
            arr[tail]=n;
            tail++;
        }

    }
    public int get(){
        //先判断队列是否为空
        if (isEmpty()){
            return -1;
        }

        int result =arr[head];
        head++;
        return result;
    }

}
/**
 * 队列测试
 */
public class LinkedListTest {
    public static void main(String[] args) {

        Queue<Integer> queue = new LinkedList<>();

        //都能添加元素
        queue.add(1);
        queue.offer(2);
        //两者区别:当队列满时,add会抛异常,offer会返回false 更健壮更友好
        //取出元素,分为两种
        //1 只获取对头元素 不取出
        queue.peek();
        queue.element();
        //两者区别 当队列为空时  element会抛出异常  peek会返回空值
        //2 获取队头元素,同时取出
        queue.poll();
        queue.remove();
        //两者区别 当队列为空时  remove会抛出异常  poll会返回空值
    }
}

双端队列

 区别于普通队列

两端都可以入队和出队

LinkedList和ArrayDeque

第一个元素(头部)最后一个元素(头部)
抛出异常特殊值抛出异常特殊值
插入addFirst(e)offerFirst(e)addLast(e)offerLast(e)
删除removeFirst()pollFirst()removeLast()pollLast()
获取getFirst()peekFirst()getLast()peekLast()

【特别的队列】

1) 优先级队列

元素携带了相关的优先级,优先级更高的元素排在头部

PriorityQueue,提供了Comparable接口,元素可以实现其方法,改变在队列中的顺序

2)阻塞队列

当队列满的时候,等待有空余位置再存数据;当队列空的时候,等待有新的数据再读取

BlockingQueue,put()和take()方法提供了存取的阻塞逻辑

分为两种情况,一种是无限期的等,一种是指定阻塞时间

3)延迟队列

在指定时间内获取队列元素,头部元素是最近过期时间的。

DelayQueue 给顶一个接口设置延迟时间 元素会按照时间排序

队列例题1:队列最大值

队列的最大值

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:

输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

分析:

队头        队尾

[1,2,3,4,53,2,1]

在队列发生更改时  记录出最大值

增删操作时 ,都可能影响最大值的变化

如果是新增操作,比较新元素和当前最大值之间,更大的值为新最大值

如果是删除操作,删除元素如果不是最大值,那么最大值不变,如果是最大值,最大值

要更改为剩余元素最大值

使用二维的队列记录最大值的变化

原队列        max队列

[1]                       [1]

[1,2]                    [2]

[1,2,3]                 [3]

[2,3]                    [3]

[2,3,2]                 [3,2]

[2,3,2,1]              [3,2,1]

[3,2,1]                 [3,2,1]

[2,1]                    [2,1]

[2,1,4]                 [4]     

队首删   队尾加                        

    

public class MaxQueue {
    public static void main(String[] args) {
       MaxQueue obj =new MaxQueue();
       obj.push_back(2);
       obj.push_back(5);
       obj.push_back(3);
       obj.push_back(1);
       obj.push_back(4);
        System.out.println("原始队列"+obj.queue.toString());
        System.out.println("队列最大值为"+obj.max_value());
        System.out.println("删除"+obj.pop_front());
        System.out.println("队列最大值为"+obj.max_value());
        System.out.println("删除"+obj.pop_front());
        System.out.println("队列最大值为"+obj.max_value());
        System.out.println("删除"+obj.pop_front());
        System.out.println("队列最大值为"+obj.max_value());
        obj.push_back(6);
        System.out.println("增加"+6);
        System.out.println("队列最大值为"+obj.max_value());

    }
    //原始队列
    public  LinkedList<Integer> queue;
    //最大值的候选值
    public  LinkedList<Integer> max;

    public MaxQueue() {
        queue = new LinkedList<>();
        max = new LinkedList<>();

    }

    public int max_value() {
        if(max.isEmpty())return -1;
        return max.peekFirst();//队首元素

    }
    //新增元素时 ele时  ele>max 取队列中队尾元素比较,如果满足,进行覆盖并循环比较
    //                          直到把所有比它小的值都覆盖为止
    //                 ele<max  将其存到max队列中
    public void push_back(int value) {
            queue.offer(value);
            //ele>max
            while (!max.isEmpty()&&max.peekLast()<value){
                max.pollLast();//取出max队尾元素
            }
            max.add(value);
    }
    //删除的元素如果不是最大值,那么最大值不变,如果为最大值最大值更改为剩余元素最大值
    public int pop_front() {
        //如果删除的元素是最大值  从max队列中同时删掉
        if (!max.isEmpty()&&queue.peekFirst().equals(max.peekFirst())){
            max.pollFirst();//删除最大值
        }
        if(queue.isEmpty()) return -1;

        return queue.poll();
    }

}
执行结果:
原始队列[2, 5, 3, 1, 4]
队列最大值为5
删除2
队列最大值为5
删除5
队列最大值为4
删除3
队列最大值为4
增加6
队列最大值为6

队列例题2:滑动窗口最大值

滑动窗口的最大值

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 

  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

提示:

你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

分析:数组长度为n         窗口的大小为k        可形成的窗口个数  n-k+1

通过队列来记录最大值

窗口未形成阶段

遍历数组,同时更新队列

窗口已形成阶段

每次窗口移动,都是在头部移除元素,尾部增加元素

        在窗口的变化过程中        记录其最大值变化(让最大值一直出现在第一位)

        让队列中已存在的元素 和新添加的元素比较  如果存在的更小        那么覆盖

        如果存在的更大·     新添加的元素是候选        缀到队列之后

窗口                        max队列

[1  3  -1]                 [1]->[3]->[3,-1]
[3  -1  -3]                 [3.-1.-3]
 [-1  -3  5]                 [-1,-3]->[5]
[-3  5  3]                 [5,3]
 [5  3  6]                 [6]
[3  6  7]                   [7] 

窗口最大值 3 3 5 5 6 7

/**
 * 给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
 * 示例:
 * 输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
 * 输出: [3,3,5,5,6,7]
 */
public class SlideWindow {
    public static void main(String[] args) {
        int[] nums ={1,3,-1,-3,5,3,6,7};
        int k =3;
        int[] ints =maxSlidingWindow(nums,k);
        System.out.println(Arrays.toString(ints));
    }

    public static int[] maxSlidingWindow(int[] nums, int k) {
       //新建一个数组存放每个窗口的最大值
        int[] result =new int[nums.length-k+1];
        //新建队列存放最大值
        Deque<Integer> deque =new ArrayDeque<>();

        //  防止数组为空报空指针异常
        if(nums.length==0) return new int[nums.length];
        //窗口未形成阶段
        for (int i = 0; i < k; i++) {
           //每次都取队尾元素和新元素 比较 如果队尾更小,删除
            while (!deque.isEmpty()&&deque.peekLast()<nums[i]){
                deque.pollLast();
            }
            deque.offerLast(nums[i]);
        }

        //第一个窗口形成,deque的队头元素就是第一个窗口的最大值

            result[0]=deque.peekFirst();
      
        System.out.println("初始窗口最大值为"+result[0]);
        System.out.println(deque.toString());
        //窗口形成阶段,第一次移动开始
        for (int i = k; i < nums.length; i++) {
            System.out.println("-----第"+(i-k+1)+"次滑动-----");
            //删除了元素nums[i-k],添加了元素num[i]

            //删除,如果删除的为最大值,一起删除
            if (!deque.isEmpty()&&deque.peekFirst()==nums[i-k]){
                deque.pollFirst();
            }

            //新增
            while (!deque.isEmpty()&&deque.peekLast()<nums[i]){
                deque.pollLast();
            }
            deque.offerLast(nums[i]);
            //每次移动存储最大值
            result[i-k+1]=deque.peekFirst();
            System.out.println("最大值为"+result[i-k+1]);
            System.out.println(deque.toString());
        }
            return result;
    }
}
执行结果:
初始窗口最大值为3
[3, -1]
-----第1次滑动-----
最大值为3
[3, -1, -3]
-----第2次滑动-----
最大值为5
[5]
-----第3次滑动-----
最大值为5
[5, 3]
-----第4次滑动-----
最大值为6
[6]
-----第5次滑动-----
最大值为7
[7]
[3, 3, 5, 5, 6, 7]

循环队列

为了解决,队列存取数据后,空间无法 重复利用的问题,通过构造环形,重复使用

例题:设计循环队列

设计循环队列

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。

/**
 * 设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
 *
 * 循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
 *
 * 你的实现应该支持如下操作:
 *
 */
public class MyCircularQueue {
    public static void main(String[] args) {
        MyCircularQueue a = new MyCircularQueue(2);
        System.out.println("增加元素"+a.enQueue(8));
        System.out.println(Arrays.toString(a.queue));
        System.out.println("增加元素"+a.enQueue(8));
        System.out.println(Arrays.toString(a.queue));
        System.out.println("获取队首元素为"+a.Front());
        System.out.println("增加元素"+a.enQueue(4));
        System.out.println(Arrays.toString(a.queue));
        System.out.println("删除元素"+a.deQueue());
        System.out.println(Arrays.toString(a.queue));
        System.out.println("增加元素"+a.enQueue(1));
        System.out.println(Arrays.toString(a.queue));
        System.out.println("增加元素"+a.enQueue(1));
        System.out.println("队列是否为满"+a.isFull());
        System.out.println(Arrays.toString(a.queue));
        System.out.println("获取队尾元素为"+ a.Rear());
        System.out.println("队列是否为空"+a.isEmpty());
        System.out.println("获取队首元素为"+a.Front());
        System.out.println("删除元素"+a.deQueue());
        System.out.println(Arrays.toString(a.queue));
    }

    int[]queue;//存储元素数组
    int capacity;//最大容量
    int head;//头指针
    int tail;//尾指针
    int count;//实际队列长度

    // * MyCircularQueue(k): 构造器,设置队列长度为 k 。
    public MyCircularQueue(int k) {
        capacity =k;
        queue=new int[k];
        head=0;
        tail=0;
        count=0;

    }
    // * enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
    public boolean enQueue(int value) {
        if (isFull())   return false;
        queue[tail]=value;  //尾指针处插入,尾指针进一位
        tail =(tail+1)%capacity;
        count++;
        return true;
    }
    // * deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
    public boolean deQueue() {
        if (isEmpty()) return false;
        queue[head]=0;
        head=(head+1)%capacity;
        count--;
        return true;

    }
    // * Front: 从队首获取元素。如果队列为空,返回 -1 。
    public int Front() {
        if (isEmpty())  return -1;
        return queue[head];
    }
    // * Rear: 获取队尾元素。如果队列为空,返回 -1 。
    public int Rear() {
        if (isEmpty()) return -1;

        return queue[(tail-1+capacity)%capacity];
        //or return queue[tail == 0 ? capacity - 1 : tail - 1];
    }
    // * isEmpty(): 检查循环队列是否为空。
    public boolean isEmpty() {
        return count ==0;
    }
    // * isFull(): 检查循环队列是否已满。
    public boolean isFull() {
        return count==capacity;
    }
}
执行结果:
增加元素true
[8, 0]
增加元素true
[8, 8]
获取队首元素为8
增加元素false
[8, 8]
删除元素true
[0, 8]
增加元素true
[1, 8]
增加元素false
队列是否为满true
[1, 8]
获取队尾元素为1
队列是否为空false
获取队首元素为8
删除元素true
[1, 0]

栈 Stack

        在同一端进行插入和删除    遵循的是先进后出/后进先出LIFO(Last in first out)的原则

存入数据——进栈、压栈

取出数据——出栈、弹栈

栈是有记忆的,比如回退功能用的就是栈

栈的实现方式:

        数组(顺序栈)

        队列实现栈(链栈)

栈例题1:有效的括号

有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

示例 1:

输入:s = "()"
输出:true
示例 2:

输入:s = "()[]{}"
输出:true
示例 3:

输入:s = "(]"
输出:false
示例 4:

输入:s = "([)]"
输出:false
示例 5:

输入:s = "{[]}"
输出:true 

/**
 * 给定一个只包括 '(',')','{','}','[',']'的字符串 s ,判断字符串是否有效。
 *
 * 有效字符串需满足:
 *
 * 左括号必须用相同类型的右括号闭合。
 * 左括号必须以正确的顺序闭合。
 *
 * 示例 1:
 * 输入:s = "()" 输出:true
 * 示例2:
 * 输入:s = "()[]{}"输出:true
 * 示例3:
 * 输入:s = "(]"输出:false
 * 示例4:
 * 输入:s = "([)]"输出:false
 * 示例5:
 * 输入:s = "{[]}"输出:true
 */
public class ValidParenthese {

    public static void main(String[] args) {

        System.out.println(isValid("()[]{}"));
        System.out.println(isValid("(]"));
        System.out.println(isValid("([)]"));
        System.out.println(isValid("{[]}"));
        System.out.println(isValid("{["));
        System.out.println(isValid("]}"));
        System.out.println("----------------------");
        System.out.println(isValid1("()[]{}"));
        System.out.println(isValid1("(]"));
        System.out.println(isValid1("([)]"));
        System.out.println(isValid1("{[]}"));
        System.out.println(isValid1("{["));
        System.out.println(isValid1("]}"));
    }
    //法1:栈存左括号,出现右括号相同就抵消
    //如果是右括号 寻找栈顶元素 能否进行匹配 匹配不上 无效
    //如果是左括号 直接压入栈中
    //  当遍历完成的时候 如果不是空栈 说明左括号未被匹配到 无效 反之,有效
    public static boolean isValid(String s) {
        Map<Character,Character> map =new HashMap<>();
        map.put('(',')');
        map.put('[',']');
        map.put('{','}');
        char[] arr =s.toCharArray();
        Stack<Character> stack = new Stack<>();
        for (char c : arr) {
            if (map.containsKey(c)){
                //左括号
                stack.push(c);
            }else {
                //右括号
                if (stack.empty()){
                    System.out.println("右括号出现,但左括号未出现");
                    return false;
                }
                char top =stack.peek();
                if (!map.get(top).equals(c)){
                    System.out.println("右括号出现,但不是与之对应的左括号");
                    return false;
                }
                //弹出左括号,已被抵消
                stack.pop();

            }

        }
        if (!stack.empty()){
            System.out.println("左括号出现,但右括号仍未出现");
            return false;
        }
        return true;
    }
    //法2:当出现左括号时,直接存入右括号,当出现右括号时,直接取出栈顶元素
    public static boolean isValid1(String s) {
        Stack<Character> stack = new Stack<>();
        for (char c : s.toCharArray()) {
            if (c=='('){
                stack.push(')');
            }else if (c=='['){
                stack.push(']');
            }else if (c=='{'){
                stack.push('}');
            }
            else {
                if (stack.empty()||c!=stack.pop()){//取出栈顶元素比较
                    System.out.println("右括号出现,但左括号未出现");
                    return false;
                }
            }
        }

        return stack.empty();//最终为空则有效括号
    }
}
执行结果:
true
右括号出现,但不是与之对应的左括号
false
右括号出现,但不是与之对应的左括号
false
true
左括号出现,但右括号仍未出现
false
右括号出现,但左括号未出现
false
----------------------
true
右括号出现,但左括号未出现
false
右括号出现,但左括号未出现
false
true
false
右括号出现,但左括号未出现
false

栈的数组实现方式

//数组实现栈
public class MyStack {

    public static void main(String[] args) {
        MyStack myStack = new MyStack(3);
        myStack.push(1);
        myStack.push(2);
        myStack.push(3);
        System.out.println(myStack.peek());
        System.out.println(myStack.pop());
        System.out.println(myStack.pop());
        System.out.println(myStack.pop());
        System.out.println(myStack.isEmpty());
        System.out.println(myStack.isFull());

    }

    //存储数据
    int[] array;
    //最大容量
    int maxSize;
    //栈指针
    int top;
    public MyStack(int size){
        maxSize=size;
        array=new int[maxSize];
        top=-1;
    }
    public void push(int value){
        if (top<maxSize-1){
            top++;
            array[top]=value;
        }
    }
    public int pop(){
        int result = array[top];
        top--;
        return result;
    }
    public int peek(){
        return array[top];
    }
    public boolean isEmpty(){
        return  top==-1;
    }
    public boolean isFull(){
        return top==maxSize-1;
    }
}
执行结果:
3
3
2
1
true
false

栈例题2:队列实现栈

用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

注意:

你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

/**
 * 队列实现栈
 * 法1:额外的队列  作为临时空间  在取数据时  存储队列其他元素    知道找队尾元素
 * 操作之后    再将临时队列中的元素移回
 */
public class MyStackByQueue {

    public static void main(String[] args) {
        MyStackByQueue obj = new MyStackByQueue();
        obj.push(1);
        obj.push(2);
        int param_2 = obj.top();
        int param_3 = obj.pop();
        boolean param_4 = obj.empty();
        System.out.println(param_2+" "+param_3+" "+param_4);
    }

    Queue<Integer>  dataQueue =new LinkedList<>();//存储数据的队列
    Queue<Integer>  tempQueue =new LinkedList<>();//临时队列

    int top;//栈顶元素

    public void push(int x) {
        dataQueue.add(x);
        top=x;

    }

    public int pop() {
        while (dataQueue.size()>1){
            top=dataQueue.poll();
            tempQueue.add(top);
        }
        int num =dataQueue.poll();
        //将dataQueue元素移回去
        Queue temp = dataQueue;
        dataQueue = tempQueue;
        tempQueue=temp;

        return num;
    }

    public int top() {
        return top;
    }

    public boolean empty() {
        return dataQueue.size()==0;
    }
}
执行结果:
2 2 false

/**
 * 队列实现栈
 * 法2:不使用额外的队列  每次存数据时  颠倒其位置   保存顺序满足栈的处理顺序
 */
public class MyStackByQueue2 {

    public static void main(String[] args) {
        MyStackByQueue2 obj = new MyStackByQueue2();
        obj.push(1);
        obj.push(2);
        int param_2 = obj.top();
        int param_3 = obj.pop();
        boolean param_4 = obj.empty();
        System.out.println(param_2+" "+param_3+" "+param_4);
    }

    Queue<Integer>  dataQueue =new LinkedList<>();//存储数据的队列



    public void push(int x) {
        dataQueue.add(x);
        //把对头的元素拿出来,放到队尾,颠倒 size-1次
        int size =dataQueue.size();
      while (size>1){
            int tail = dataQueue.poll();
            dataQueue.offer(tail);
            size--;

      }

    }

    public int pop() {


        return dataQueue.poll();
    }

    public int top() {
        return dataQueue.peek();
    }

    public boolean empty() {
        return dataQueue.size()==0;
    }
}
执行结果:
2 2 false

 栈例题3:栈实现队列 

用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:

你 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

/**
 * 队列用栈的实现方式
 */
public class MyQueueByStack{
    public static void main(String[] args) {
        MyQueueByStack myQueue = new MyQueueByStack();
        myQueue.push(1); // queue is: [1]
        myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
        System.out.println(myQueue.stack1);
        System.out.println( myQueue.peek()); // return 1
        System.out.println(myQueue.pop()); // return 1, queue is [2]
        System.out.println(myQueue.empty()); // return false

    }
    Stack<Integer> stack1 = new Stack<>();//入栈
    Stack<Integer> stack2 = new Stack<>();//出栈

    public void push(int x) {
        stack1.push(x);

    }

    public int pop() {
        //如果stack2为空,就将stack1出栈,stack2入栈
        if (stack2.empty()) {
            while(stack1.size()>0){
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop();
    }

    public int peek() {
        //如果stack2为空,就将stack1出栈,stack2入栈
        if (stack2.empty()) {
            while(stack1.size()>0){
                stack2.push(stack1.pop());
            }
        }
        return stack2.peek();

    }

    public boolean empty() {
        //stack1和stack2两个都空才为空
        return stack2.empty()&&stack1.empty();
    }
}
执行结果:
[1, 2]
1
1
false

 栈例题4:栈的最小值

栈的最小值

请设计一个栈,除了常规栈支持的pop与push函数以外,还支持min函数,该函数返回栈元素中的最小值。执行push、pop和min操作的时间复杂度必须为O(1)。
示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.

/**
 * 法1:尝试用额外的栈  来记录数据栈的最小值
 * dataStack    -2 0 -3 1           2  0  3  0
 * minStack     -2 -2 -3 -3         2  0  0  0
 */
public class MinStack {
    Stack<Integer> stack = new Stack<>();
    Stack<Integer> min = new Stack<>();//栈顶存储栈的最小值

    public void push(int x) {
        stack.push(x);
        if (!min.empty()&&min.peek()<x){
            min.push(min.peek());
        }else {
            min.push(x);
        }

    }

    public void pop() {
        stack.pop();
        min.pop();
    }

    public int top() {
      return  stack.peek();
    }

    public int getMin() {
         return  min.peek();
      
    }
}
/**
 * 法2:不使用额外的栈   使用变量来标记栈的最小元素
 * -2 0 -3 1
 * dataStack    -2 0 -2 -3 1     存
 * min  -2 -> -3
 * dataStack    -2 0        取
 * min  -2
 * 0 1 0
 * 0 1 0 0
 * 0
 */
public class MinStack1 {
    Stack<Integer> stack = new Stack<>();
        int min =Integer.MAX_VALUE;//初始化min

    public void push(int x) {
        //如果新元素使最小值发生变化,则会存储两个值(原来的最小值,当前的最小值)
        if(min>=x){
            if (!stack.empty()){
                stack.push(min);
            }
            //最小值重新赋值
            min=x;
        }
        stack.push(x);
    }

    public void pop() {
        if (stack.empty()) return;
        if (stack.size()==1){
            min=Integer.MAX_VALUE;
        }else if (min==stack.peek()){
            stack.pop();
            min=stack.peek();
        }
        //如果移除的不是最小值,直接pop,
        // 如果移除的是最小值, 前面移除一次,再移除之前保存的最小值
        stack.pop();
        min=stack.peek();
    }

    public int top() {
        return stack.peek();
    }

    public int getMin() {
        return min;
    }
}

 栈例题5:下一个更大元素

下一个更大元素 I

nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。

给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。

对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。

返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素 。

示例 1:

输入:nums1 = [4,1,2], nums2 = [1,3,4,2].
输出:[-1,3,-1]
解释:nums1 中每个值的下一个更大元素如下所述:
- 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
- 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
- 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
示例 2:

输入:nums1 = [2,4], nums2 = [1,2,3,4].
输出:[3,-1]
解释:nums1 中每个值的下一个更大元素如下所述:
- 2 ,用加粗斜体标识,nums2 = [1,2,3,4]。下一个更大元素是 3 。
- 4 ,用加粗斜体标识,nums2 = [1,2,3,4]。不存在下一个更大元素,所以答案是 -1 。

/**
 * 用栈实现下一个最大值
 * arr[2,3,5,1,0,7,4]
 * map<2,3> <3,5> <0,7> <1,7> <5,7> <4,-1> <7,-1>
 * stack[2] ->  3
 * stack[3] ->  5
 * stack[5] ->  1
 * stack[5,1] ->  0
 * stack[5,1,0] ->  7
 * stack[7] ->  4
 * stack[7,4]
 *
 */
public class NextGreaterNum {

    public static void main(String[] args) {
        int[] nums1 ={2,3,5,1,0,7,4};
        int[] nums2 ={2,3,5,1,0,7,4};
        int[] result = nextGreaterElement(nums1,nums2);
        System.out.println(Arrays.toString(result));
    }

    public static int[] nextGreaterElement(int[] nums1, int[] nums2) {

        //暂时忽略nums1  直接求取nums2中  每一个元素的下一个最大值
        Stack<Integer>  stack  =new Stack<>();
        Map<Integer,Integer> map = new HashMap<>();//键存储元素,值存储其下一个最大值

        int[] result = new int[nums1.length];

        for (int i = 0; i < nums2.length; i++) {
            //比较  栈顶元素和新元素
            while(!stack.empty()&&nums2[i]>stack.peek()){
                map.put(stack.pop(),nums2[i]);
            }
            //如果新元素更小,直接入栈  等待后面出现的更大元素
            //如果找到更大元素  存入map后  新元素仍要入栈
           stack.push(nums2[i]);
            
        }
        //如果栈中还有元素  代表没出现过更大元素
        while(!stack.empty()){
            map.put(stack.pop(),-1);
        }
        for (int i = 0; i <nums1.length; i++) {
             result[i]=map.get(nums1[i]);
        }
        
        return result;
    }
}
[3, 5, -1, -1, 7, -1, -1]

算数表达式:

        3+4-5=2

        3+4*5=23

        前缀、中缀、后缀表达式(指的是运算符的位置)

        前缀又叫波兰表达式        后缀又叫逆波兰表达式

        3+4-5

中缀:3+4-5

前缀:+--543

后缀:34+5-

栈例题:逆波兰表达式

逆波兰表达式求值

根据 逆波兰表示法,求表达式的值。

有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

注意 两个整数之间的除法只保留整数部分。

可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例 1:

输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:

输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3:

输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22 


public class ReversePolishNotation {

    public static void main(String[] args) {

//        String[] str1 = {"2","1","+","3","*"};
//        String[] str2 = {"4","13","5","/","+"};
//        String[] str3 = {"10","6","9","3","+","-11","*","/","*","17","+","5","+"};
//        System.out.println(evalRPN(str1));
//        System.out.println(evalRPN(str2));
//        System.out.println(evalRPN(str3));


       transfer("1+((2+3)*4)-5");

    }


    public static int evalRPN(String[] tokens) {
        Stack<String> stack = new Stack<>();
        int nums1,nums2;
        for (int i = 0; i < tokens.length; i++) {
           switch (tokens[i]){
               case "+":
                    nums1=Integer.parseInt(stack.pop());
                    nums2=Integer.parseInt(stack.pop());
                    stack.push(nums2+nums1+"");
                   break;
               case "-":
                   nums1=Integer.parseInt(stack.pop());
                   nums2=Integer.parseInt(stack.pop());
                   stack.push(nums2-nums1+"");
                   break;
               case "*":
                   nums1=Integer.parseInt(stack.pop());
                   nums2=Integer.parseInt(stack.pop());
                   stack.push(nums2*nums1+"");break;
               case "/":
                   nums1=Integer.parseInt(stack.pop());
                   nums2=Integer.parseInt(stack.pop());
                   stack.push(nums2/nums1+"");break;
               default:
                   //如果是数字,直接入栈
                   stack.push(tokens[i]);
           }
            
        }
        return Integer.parseInt(stack.pop());
    }
    //中缀转后缀
    public static void transfer(String str){

            Stack<Character> stack1 = new Stack<>();//运算符栈
            Stack<Character> stack2 = new Stack<>();//中间结果栈

            char[]  chars = str.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            switch (chars[i]){
                //判断 是运算符 还是操作数  以及 具体的加减乘除或括号
                case '+':
                case '-':
                case '*':
                case '/':
                    //有两种情况     直接存入s1
                    //如果s1为空  或者栈顶元素是括号
                    //如果  当前运算符 优先级比栈顶元素高,直接存入
                    while (true) {


                        if (stack1.empty() || stack1.peek().equals('(')
                                || compare(chars[i], stack1.peek()) > 0) {
                            stack1.push(chars[i]);
                            break;
                        }
                            stack2.push(stack1.pop());
                    }
                    break;
                case '(':
                    stack1.push(chars[i]);
                    break;
                case ')':
                    while (!stack1.peek().equals('(')){
                        //中间运算符     取出并存入s2
                        stack2.push(stack1.pop());
                    }
                    //此时左括号被抵消,所以取出
                    stack1.pop();
                    break;
                default:
                    //是数字
                    stack2.push(chars[i]);
            }
        }
        while (!stack1.empty()){
            stack2.push(stack1.pop());
        }
        //将栈中元素 倒序输出到数组中
        String[] arr = new String[stack2.size()];
        int size = stack2.size()-1;
        while (!stack2.isEmpty()){
            arr[size]=stack2.pop()+"";
            size--;
        }
        System.out.println(Arrays.toString(arr));
    }
    //定义优先级   乘除大于加减  1代表c1优先级高 -1代表c2优先级高  0代表相同
    public static int compare(char c1,char c2){
        if (c1=='+'|| c1=='-'){
            if(c2=='*'||c2=='/'){
                return -1;
            }
            return 0;
        }
        if (c1=='*'|| c1=='/'){
            if(c2=='+'||c2=='-'){
                return 1;
            }
            return 0;
        }
        return 0;
    }

}
9
6
22

 拓展:中缀转后缀表达式(代码为上)

分析:

中缀-->后缀

1+((2+3)*4)-5

元素                s2(中间结果栈)                s1(运算符栈)                说明

1                        1

+                                                                        +

(                                                                         + (

(                                                                         + ( (

2                        1 2

+                                                                        + ( ( +

3                        1 2 3

)                         1 2 3 +                                     + (

                                                                       出现右括号  在s1中找到对应的左括号

                                                                        将中间元素存入s2

*                                                                        + ( *

4                        1 2 3 + 4

)                         1 2 3 + 4 *                               +

-                          1 2 3 + 4 *  +                           -

                                                                        当新运算符和栈顶运算符同一优先级   

                                                                        取出s1元素,存入s2  

5                         1 2 3 + 4 *  +  5       

null                      1 2 3 + 4 *  +  5  -     

                        

[1, 2, 3, +, 4, *, +, 5, -]

散列表(哈希表)

什么是哈希表?

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

记录的存储位置=f(关键字)

这里的对应关系f称为散列函数,又称为哈希(Hash函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。

哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。(或者:把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。)
    而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。

 Hash的应用

1、Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值. 也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。

2、查找:哈希表,又称为散列,是一种更加快捷的查找技术。我们之前的查找,都是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等,缩小范围,继续查找。而哈希表是完全另外一种思路:当我知道key值以后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次又一次的查找!

举一个例子,假如我的数组A中,第i个元素里面装的key就是i,那么数字3肯定是在第3个位置,数字10肯定是在第10个位置。哈希表就是利用利用这种基本的思想,建立一个从key到位置的函数,然后进行直接计算查找。

3、Hash表在海量数据处理中有着广泛应用。

散列表的查找步骤 

当存储记录时,通过散列函数计算出记录的散列地址

当查找记录时,我们通过同样的是散列函数计算记录的散列地址,并按此散列地址访问该记录

哈希算法:

将任意长度的输入,通过算法 ,转换为固定长度的输出

散列法:

1)直接寻址法,取关键字或关键字的线性函数        作为散列地址

2)除留取模法,对关键字或关键字的部分取模        作为散列地址

        取模的除数,一般为素数/质数

3)取随机数法,使用随机函数,取关键字的随机值        作为散列地址

4)数字分析法,根据数字的特性,经过分析,取部分进行计算(如手机号后四位等)

5)平方取中法

      先求平方,取中间几位        作为散列地址

6)折叠法

取关键字的几部分

散列冲突(哈希冲突)

原因——抽屉原理

map中不同映射指向同一个地址        哈希碰撞

解决办法:再找一个空闲位置

具体如下

1)线性探测

        key01        ——1号柜

                                如果满了        顺延的下一个位置        ——2号柜子

2)二次探测

        如果满了        按照一定规律顺延

                如以二次方顺延          value        value^2        value+2^2

3)双重哈希

        使用两种哈希函数        第一个位置被占用时        计算第二个

4)链表法

        让一个位置        存储多个value

哈希应用

哈希例题1:两数之和

两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

public class TwoSum {

    public static void main(String[] args) {
        int[] ints ={2,7,11,15};
        System.out.println(Arrays.toString(twoSum(ints,26)));
        System.out.println(Arrays.toString(twoSum1(ints,26)));
        System.out.println(Arrays.toString(twoSum2(ints,26)));
    }

    //法一,暴力破解;遍历每个元素,查找后续元素与其相加的和   是否等于target
    //依次遍历出每两个元素的和
    public static int[] twoSum(int[] nums, int target) {
        //遍历每个元素    查找后续元素与其相加的和    是否等于target
        for (int i = 0; i < nums.length; i++) {
            for (int j = i+1; j < nums.length; j++) {
               if(nums[i]+nums[j]==target){
                   return  new int[]{i,j};
               }

            }

        }
        return new int[]{-1,-1};
    }

    //法二:倒推法
    //使用额外容器存储  快速找到是否存在某个值
    //<元素值,索引位置>    hashmap

    public static int[] twoSum1(int[] nums, int target) {
        //使用map存储 <元素值,索引位置>
        Map<Integer,Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i],i );

        }
        //再次遍历,
        //用map查找另一个值,另一个值相加的
        for (int i = 0; i < nums.length; i++) {
            int num = target-nums[i];
            //数组中同一元素不能使用两遍
            if (map.containsKey(num)&&map.get(num)!=i){
                return new int[]{i,map.get(num)};
            }

        }
        return new int[]{-1,-1};
    }

    //法三:一次遍历,哈希,合并法一法二
    //边遍历边修改
    public static int[] twoSum2(int[] nums, int target) {
        Map<Integer,Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int num = target-nums[i];
            if (map.containsKey(num)&&map.get(num)!=i){
                return new int[]{map.get(num),i};
            }
            map.put(nums[i],i);
            
        }
        return new int[]{-1,-1};
    }
   
}
[2, 3]
[3, 2]
[3, 2]

哈希例题2:两数之和

找不同

给定两个字符串 s 和 t ,它们只包含小写字母。

字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。

请找出在 t 中被添加的字母。

示例 1:

输入:s = "abcd", t = "abcde"
输出:"e"
解释:'e' 是那个被添加的字母。
示例 2:

输入:s = "", t = "y"
输出:"y"

public class FindDifferent {

    public static void main(String[] args) {
        System.out.println(findTheDifference("abcd","abcde"));
        System.out.println(findTheDifference("abcd","abcda"));
        System.out.println("---------------");
        System.out.println(findTheDifference1("abcd","abcde"));
        System.out.println(findTheDifference1("abcd","abcda"));
        System.out.println("---------------");
        System.out.println(findTheDifference2("abcd","abcde"));
        System.out.println(findTheDifference2("abcd","abcda"));
        System.out.println("---------------");
        System.out.println(findTheDifference3("abcd","abcde"));
        System.out.println(findTheDifference3("abcd","abcda"));
    }
    //法一:
    //使用map分别记录 s和t中<字母,出现次数>
    //t中也出现了,就抵消,
    //t出现多了。或者t有s没有,即为新增字母
    public static char findTheDifference(String s, String t) {
        Map<Character,Integer> map = new HashMap<>();
        for (Character character : s.toCharArray()) {
            if (map.containsKey(character)){
                Integer integer = map.get(character)+1;
                map.put(character,integer);

                continue;
            }
            map.put(character,1);
        }
        //查找t中出现多了一次,即再次循环为map中value0时
        //或者t中出现map中不存在,都为新增元素
        for (Character character : t.toCharArray()) {
            if (!map.containsKey(character)){
                return  character;
            }
            if (map.containsKey(character)&&map.get(character)>0){
                //出现就抵消一次
                Integer integer = map.get(character)-1;
                map.put(character,integer);
                continue;
            }else {
                return character;
            }

        }
            return '-';
    }

    //法二:字符串替换法     replace()
    //遍历s,将s中t替换
    public static char findTheDifference1(String s, String t) {
        for (Object o : s.toCharArray()) {
            //替换第一个出现的位置
            t=t.replaceFirst(o.toString(),"");

        }
        return t.toCharArray()[0];

    }

    //法三:根据ASCII码特性,s和t字母ascii码之和的差值为多出来的字母ascii码值
    public static char findTheDifference2(String s, String t) {
        int sSum =0,tSum=0;
        for (byte aByte : s.getBytes()) {
            sSum+=aByte;
        }
        for (byte aByte : t.getBytes()) {
            tSum+=aByte;
        }
       return (char) (tSum-sSum);
    }

    //法四:异或运算  利用同异或等于0,异或0等于本身特性
    public static char findTheDifference3(String s, String t) {
        int result =0;
        for (char c : s.toCharArray()) {
            result^=c;
        }
        for (char c : t.toCharArray()) {
            result^=c;
        }
        return (char) result;
    }

}
e
a
---------------
e
a
---------------
e
a
---------------
e
a

递归

斐波那契数列(Fibonacci sequence)

        又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(≥ 2,∈ N*)

public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(fib(6));
    }
    //返回第n个月有多少只兔子
    public static int fib(int N){
        if (N==1) return 1;
        if (N==2) return 1;
        System.out.println("求第"+N+"个月的兔子数量");
        System.out.println("转化为求第"+(N-1)+"个月和第"+(N-2)+"个月的兔子数量");
         return fib(N-1)+fib(N-2);
    }
}
求第6个月的兔子数量
转化为求第5个月和第4个月的兔子数量
求第5个月的兔子数量
转化为求第4个月和第3个月的兔子数量
求第4个月的兔子数量
转化为求第3个月和第2个月的兔子数量
求第3个月的兔子数量
转化为求第2个月和第1个月的兔子数量
求第3个月的兔子数量
转化为求第2个月和第1个月的兔子数量
求第4个月的兔子数量
转化为求第3个月和第2个月的兔子数量
求第3个月的兔子数量
转化为求第2个月和第1个月的兔子数量
8

大部分递归可以转化为迭代处理

main函数:System.out.println(fib(6));

public static int fib1(int N){
        //6 —— 0 1 2 3 4 5
        //fib(0)=0 有时需要处理
        if (N<=1)   return 1;
        int[] arr =new int[N];
        arr[0]=1;
        arr[1]=1;
        for (int i = 2; i < arr.length; i++) {
            arr[i]=arr[i-1]+arr[i-2];
            
        }
        return arr[N-1];
    }

结果:8

汉诺塔问题(不能通过迭代处理)

/**
 * (1)n == 1
 *              第1次  1号盘  A---->C       sum = 1 次
 *        (2)  n == 2
 *              第1次  1号盘  A---->B
 *              第2次  2号盘  A---->C
 *              第3次  1号盘  B---->C        sum = 3 次
 *   (3)n == 3
 *         第1次  1号盘  A---->C
 *         第2次  2号盘  A---->B
 *         第3次  1号盘  C---->B
 *         第4次  3号盘  A---->C
 *         第5次  1号盘  B---->A
 *         第6次  2号盘  B---->C
 *         第7次  1号盘  A---->C        sum = 7 次
 *
 * 不难发现规律:1个圆盘的次数 2的1次方减1
 *        2个圆盘的次数 2的2次方减1
 *                          3个圆盘的次数 2的3次方减1
 *                          。  。   。    。   。
 *                          n个圆盘的次数 2的n次方减1
 *  故:移动次数为:2^n - 1
 */
public class Hanoi {

    public static void main(String[] args) {
        //三个圆盘  A->C    A->B    C->B(把前两个圆盘 从A移动到B)
        //        A->C    (移动最大的圆盘)
        //        B->A    B->C    A->C (再把前两个圆盘 从B移动到C)
        hanoi(3,'A','B','C');
       System.out.println((int) time(3)+"次");

    }

    //四个参数  有n个圆盘  需要从A柱子移动到C 经由B
    //                      起始      中间      终点
    public static void hanoi(int n,char A,char B,char C){
        //出口
        if(n==1){
            System.out.println(A+"->"+C);
            return;
        }
        //  先把前N-1个圆盘 从A移动到B (经由C)
        //  再把最大的圆盘  从A移动到C
        //   最后把前N-1个圆盘  从B移动到C(经由A)
        hanoi(n-1,A,C,B);
        System.out.println(A+"->"+C);
        hanoi(n-1,B,A,C);

    }
    public static double time(int n){
      return Math.pow(2,3)-1;
    }
}
A->C
A->B
C->B
A->C
B->A
B->C
A->C
7

阶乘

public class Factorial {
    //递归计算n的阶乘
    public static void main(String[] args) {
        System.out.println(factorial(6));
        System.out.println(factorial1(6));
        System.out.println(factorial2(6));
    }
    //递归计算n的阶乘
    public  static int factorial(int n){
        if (n==1) return 1;
        return n*factorial(n-1);
    }
    //数组法
    public  static int factorial1(int n){
        if (n<=1)   return 1;
        int[] arr =new int[n+1];
        arr[0]=1;
        arr[1]=1;
        for (int i = 2; i < arr.length; i++) {
           arr[i]=arr[i-1]*i;

        }
        return arr[n];
    }
    //循环法,n必须大于0
    public  static int factorial2(int n){

        int result=1;
        while(n>1){
            result*=n*(n-1);
            n-=2;
        }
        return result;
    }
}
720
720
720

倒叙输出正整数

/**
 * 倒叙输出正整数:
 * n=12345;
 * m=54321
 */
public class OutputIntegerByReverse {

    public static void main(String[] args) {
        System.out.println(optInt(413586));
        System.out.println(optInt1(413586));
        optInt2(413586);
    }
    //法一,递归法:利用个位数是前一位的特性
    //(1)         1
    //(12)       2+1
    //(123)       3+(12)
    public static int optInt(int n){
        if(n<10)return n;
        System.out.print(n%10);
        return optInt(n/10);

    }
    //法二:循环法
    //定义一个String量,拼接字符
    public static int optInt1(int n){
            String s="";
        while (n>0){
            s+=n%10;
            n/=10;
        }

        return Integer.parseInt(s) ;
    }
    //法三:数组法
    public static void optInt2(int n){

        int[] arr =new int[Integer.toString(n).length()];
        int i=0;
        while(n>0){
            arr[i]=n%10;
            n/=10;
            System.out.print(arr[i]);
            i++;

        }
    }
}
685314
685314
685314

        树定义和基本术语
定义
树(Tree)是n(n≥0)个节点的有限集T,并且当n>0时满足下列条件:
     (1)有且仅有一个特定的称为根(Root)的节点;
     (2)当n>1时,其余节点可以划分为m(m>0)个互不相交的有限集T1、T2 、…、Tm,每个集Ti(1≤i≤m)均为树,且称为树T的子树(SubTree)。
    特别地,不含任何节点(即n=0)的树,称为空树。

基本术语
节点:存储数据元素和指向子树的链接,由数据元素和构造数据元素之间关系的引用组成。
孩子节点:树中一个节点的子树的根结点称为这个节点的孩子节点,如图1中的A的孩子节点有B、C、D
双亲节点:树中某个节点有孩子节点(即该节点的度不为0),该节点称为它孩子节点的双亲节点,也叫前驱节点。双亲节点和孩子节点是相互的,如图1中,A的孩子节点是B、C、D,B、C、D的双亲节点是A。
兄弟节点:具有相同双亲节点(即同一个前驱)的节点称为兄弟节点,如图1中B、B、D为兄弟节点。
节点的度:节点所有子树的个数称为该节点的度,如图1,A的度为3,B的度为2.
树的度:树中所有节点的度的最大值称为树的度,如图1的度为3.
叶子节点:度为0的节点称为叶子节点,也叫终端节点。如图1的K、L、F、G、M、I、J
分支节点:度不为0的节点称为分支节点,也叫非终端节点。如图1的A、B、C、D、E、H
节点的层次:从根节点到树中某节点所经路径的分支数称为该节点的层次。根节点的层次一般为1(也可以自己定义为0),这样,其它节点的层次是其双亲节点的层次加1.
树的深度:树中所有节点的层次的最大值称为该树的深度(也就是最下面那个节点的层次)。
有序树和无序树:树中任意一个节点的各子树按从左到右是有序的,称为有序树,否则称为无序树。
树的抽象数据类型描述
数据元素:具有相同特性的数据元素的集合。
结构关系:树中数据元素间的结构关系由树的定义确定。

树的实现
树是一种递归结构,表示方式一般有孩子表示法和孩子兄弟表示法两种。树实现方式有很多种、有可以由广义表的递归实现,也可以有二叉树实现,其中最常见的是将树用孩子兄弟表示法转化成二叉树来实现。

二叉树

二叉树是一种特殊的树,每个节点最多只能有两个子节点。

二叉搜索树要求:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

满二叉树

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

 

 完全二叉树

        叶子节点只能出现在最下层,并且最下层的节点都集中在该层最左边的若干位置的二叉树

 斜树

 

二叉树例题:树的深度

二叉树的深度

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

例如:

给定二叉树 [3,9,20,null,null,15,7],

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

public class Depth {

    public static void main(String[] args) {

//                 3
//                / \
//               9  20
//                  /  \
//                 15   7
        TreeNode treeNode = new TreeNode(3);
        TreeNode leftNote = new TreeNode(9);
        TreeNode rightNote = new TreeNode(20);
        TreeNode subLeft = new TreeNode(15);
        TreeNode subRight = new TreeNode(7);
        treeNode.left=leftNote;
        treeNode.right=rightNote;
        rightNote.left=subLeft;
        rightNote.right=subRight;
        System.out.println(maxDepth(treeNode));
    }

    public static int maxDepth(TreeNode root) {
        //树的深度  左子树的深度和右子树的深度中  更夫的值    +1

        if (root==null)return 0;
        int leftDepth  = maxDepth(root.left);
        int rightDepth  = maxDepth(root.right);

        return Math.max(leftDepth,rightDepth)+1;
    }

}



public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int x) {
        val = x;
    }
}
3

新建完全二叉树

public class CreateTree {

    public static void main(String[] args) {
        TreeNode node = createTree();
        System.out.println();
    }

    private static int[] array ={1,2,3,4,5};
    private static List<TreeNode> nodeList =new LinkedList<>();
    //完全二叉树
    public static TreeNode createTree(){
        //构造节点
        for (int i = 0; i < array.length; i++) {
             TreeNode node = new TreeNode(array[i]);
             nodeList.add(node);
        }
        //构造节点之间的关系
        for (int i = 0;i<nodeList.size()/2;i++){
            TreeNode node = nodeList.get(i);
            node.left=nodeList.get(i*2+1);
            //最后一个父节点   可能没有右孩子   需要额外判断(奇数)
            if (i*2+2<nodeList.size()){
                node.right=nodeList.get(i*2+2);
            }

        }
        return nodeList.get(0);
    }

}

相同的树

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:


输入:p = [1,2,3], q = [1,2,3]
输出:true
示例 2:

 


输入:p = [1,2], q = [1,null,2]
输出:false
示例 3:


输入:p = [1,2,1], q = [1,1,2]
输出:false

   

public class IsSameTree {


    //两棵树,如果有左右子树,树A的左子树==树B的左子树,同时树A的右子树==树B的右子树
    //如果两棵树都是空,是相同的
    //如果一棵为空,一棵不为空,是不同的
    //如果数值不同,也是不同的
    public boolean isSameTree(TreeNode p, TreeNode q) {

        //递归出口
        if (p==q&&p==null){
            return true;
        }
        if (p.val!=q.val){
            return false;
        }
        if (p==null||q==null){
            return false;
        }
        //递归规律
        return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
    }

}

先序:根左右        中序:左根右        后序:左右根 

完全二叉树 

递归实现遍历完全二叉树 

//递归实现遍历
public class TreeSearch {

    public static void main(String[] args) {
        TreeNode node=CreateTree.createTree();
        System.out.println("先序:");
        preOrder(node);
        System.out.println("\n中序:");
        midOrder(node);
        System.out.println("\n后序:");
        lasOrder(node);
    }

    //先序遍历
    public static void preOrder(TreeNode node){
        //递归出口
        if(node==null) return;

        System.out.print(node.val+" ");//根
        preOrder(node.left);//左
        preOrder(node.right);//右
    }

    //中序遍历
    public static void midOrder(TreeNode node){
        //递归出口
        if(node==null) return;


        midOrder(node.left);//左
        System.out.print(node.val+" ");//根
       midOrder(node.right);//右
    }

    //后序遍历
    public static void lasOrder(TreeNode node){
        //递归出口
        if(node==null) return;
        lasOrder(node.left);//左
        lasOrder(node.right);//右
        System.out.print(node.val+" ");//根
    }
}
先序:
1 2 4 8 9 5 3 6 7 
中序:
8 4 9 2 5 1 6 3 7 
后序:
8 9 4 5 2 6 7 3 1 

广度优先遍历 

public class TreeSearchByQueue {

    public static void main(String[] args) {
        TreeNode node=CreateTree.createTree();
        bfs(node);
    }
        //广度优先遍历的实现
        //通过队列实现    从根节点开始存储到队列中
        //对队列元素的处理是   将队头节点的孩子存入队列中,取出队头节点
        //直到队列为空    所有节点处理完成    同时节点的顺序是按照层级的

        public  static void bfs(TreeNode node){
            if (node==null)return;

            Queue<TreeNode> queue =new LinkedList<>();
            queue.offer(node);
            while (queue.size()>0){
                //取出队首元素
                TreeNode node1 =queue.poll();
                System.out.print(node1.val+" ");
                //如果其有左孩子或者右孩子,将其存入队列中
                if (node1.left!=null) {
                    queue.offer(node1.left);
                }
                if (node1.right!=null) {
                    queue.offer(node1.right);
                }
            }
        }
1 2 3 4 5 6 7 8 9 

非递归实现遍历完全二叉树 

/**
 *          1
 *        /  \
 *       2    3
 *     / \   / \
 *    4   5 6   7
 *   / \
 *  8   9
 */
public class TreeSearch2 {

    public static void main(String[] args) {
        TreeNode node=CreateTree.createTree();
        System.out.println("先序:");
        preOrderByLoop(node);
        System.out.println("\n中序:");
        midOrderByLoop(node);
        System.out.println("\n后序:");
        lasOrderByLoop(node);
    }

    //栈实现先序遍历
    public static void preOrderByLoop(TreeNode node){
        if(node==null) return;
        Stack<TreeNode> stack = new Stack<>();
        //使用指针记录遍历到哪个节点
        TreeNode p = node;

        while (p!=null||!stack.isEmpty()) {
            //入栈    把当前能读到的所有左孩子都存入栈中
            //入栈的方式就是先序遍历的结果
            while (p!=null){
                //先序是入栈打印
                System.out.print(p.val+" ");
                stack.push(p);
                p=p.left;
            }
            //入栈顺序:1 2 4 8 9 5 3 6 7
            //出栈顺序:8 4 9 2 5 1 6 3 7
            //1 2 4 8
            //1 2 9
            //1 5
            //3 6
            //7
            //出栈,如果 栈不为空,弹出栈顶元素,找到右孩子
            if (!stack.isEmpty()){
                p = stack.pop();
                p = p.right;//指针下移
            }
        }

    }
    //栈实现中序遍历
    public static void midOrderByLoop(TreeNode node){
        if(node==null) return;
        Stack<TreeNode> stack = new Stack<>();
        //使用指针记录遍历到哪个节点
        TreeNode p = node;

        while (p!=null||!stack.isEmpty()) {
            //入栈    把当前能读到的所有左孩子都存入栈中
            //入栈的方式就是先序遍历的结果
            while (p!=null){
                stack.push(p);
                p=p.left;
            }
            //1 2 4 8
            //1 2 9
            //1 5
            //3 6
            //7
            //出栈,如果 栈不为空,弹出栈顶元素,找到右孩子
            if (!stack.isEmpty()){
                p = stack.pop();
                //中序是出栈打印
                System.out.print(p.val+" ");
                p = p.right;//指针下移
            }
        }

    }
    //栈实现后序遍历
    public static void lasOrderByLoop(TreeNode node){
        if(node==null) return;
        Stack<TreeNode> stack = new Stack<>();
        //使用指针记录遍历到哪个节点
        TreeNode p = node;
        //记录上一次访问的节点
        TreeNode pre = node;

        while (p!=null||!stack.isEmpty()) {
            //入栈    把当前能读到的所有左孩子都存入栈中
            //入栈的方式就是先序遍历的结果
            while (p!=null){
                //先序是入栈打印
                stack.push(p);
                p=p.left;
            }
            //遍历经过的路径   左 - 根 - 右
            //  后序遍历时  根节点不从栈中弹出    要在右子树遍历后    再弹出
            //      如何判断右子树被遍历完成
            //          通过上一次遍历的是当前节点的右子树   代表此根节点也可以被遍历出来
            //1 2 4 8
            //1 2 4 9
            //1 2
            //1 2 5
            //1 3 6
            //1 3 7
            //入栈顺序:1 2 4 8 9 5 3 6 7
            //出栈顺序:8 9 4 5 2 6 7 3 1
            //节点出栈情况:
            //1)当前节点是叶子节点
            //2)上次出栈的节点是当前节点的右孩子
            if (!stack.isEmpty()){
                p = stack.pop();
                if(p.right==null||pre==p.right){
                    System.out.print(p.val+" ");
                    pre=p;
                    p=null;
                }else {
                    //不满足出栈条件,因为节点有右孩子,且没被访问到
                    stack.push(p);
                    p = p.right;//指针下移
                }
            }
        }

    }

}
先序:
1 2 4 8 9 5 3 6 7 
中序:
8 4 9 2 5 1 6 3 7 
后序:
8 9 4 5 2 6 7 3 1 

例题1:对称二叉树

对称二叉树

给你一个二叉树的根节点 root , 检查它是否轴对称。

示例 1:

输入:root = [1,2,2,3,4,4,3]
输出:true

示例 2:

输入:root = [1,2,2,null,3,null,3]
输出:false

public class Symmetric {

    public static void main(String[] args) {
        Integer[] arr = {1,2,2,null,3,null,3};
        TreeNode node = CreateTree.createTreeByArray(arr,0);
        System.out.println(isSymmetric(node));
        System.out.println(isSymmetric1(node));
    }


    //递归
    public static boolean isSymmetric(TreeNode root) {
        //排除空树的情况
        if(root==null)return true;
        //拆分成左右子树   递归判断
        return isMirror(root.left,root.right);
    }

    //  将树拆解为 左子树A    右子树B
    //      A的左子树和B的右子树要对称
    //      A的右子树和B的左子树有对称
    public static boolean isMirror(TreeNode node1,TreeNode node2){
        //递归的出口
        if (node1==null&& node2==null)  return true;
        if (node1==null||node2==null)   return false;
        if (node1.val!=node2.val)   return false;

        return isMirror(node1.left,node2.right)
                && isMirror(node1.right,node2.left);
    }

    //====================================================

    //非递归 队列法
    public static boolean isSymmetric1(TreeNode root) {
        if(root==null) return true;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root.left);
        queue.offer(root.right);

        while (!queue.isEmpty()){
            TreeNode node1 = queue.poll();
            TreeNode node2 = queue.poll();
            if (node1==null&& node2==null)  continue;
            //判断是否不对称
            if (node1==null||node2==null)   return false;
            if (node1.val!=node2.val)   return false;
            //
            queue.offer(node1.left);
            queue.offer(node2.right);
            queue.offer(node1.right);
            queue.offer(node2.left);
        }
        return true;
    }

}
false
false

例题2:翻转二叉树

翻转二叉树

 给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:

输入:root = [2,1,3]
输出:[2,3,1]
示例 3:

输入:root = []
输出:[]


 

public class InvertTree {

    /**
     *              4                               4
     *          /       \                       /       \
     *         2         7          ——————》   7          2
     *       /  \       /  \                 /  \       /  \
     *      1    3     6    9               9    6     3    1
     */
    public static void main(String[] args) {
        Integer[] arr ={4,2,7,1,3,6,9};
        TreeNode node=CreateTree.createTreeByArray(arr,0);
//        TreeNode result1 = invertTree1(node);
       TreeNode result2 = invertTree1(node);
//        TreeNode result3 = invertTree1(node);
//        TreeNode result4 = invertTree1(node);
        System.out.println();

    }

    //法一(前序遍历):先将左右子树交换 然后再分别 翻转左右子树
    public static TreeNode invertTree1(TreeNode root) {
        //递归出口
        if (root==null)return null;
        TreeNode tmp = root.left;
        root.left=root.right;
        root.right=tmp;
        invertTree1(root.left);
        invertTree1(root.right);
        return root;

    }

    //法二(中序遍历):先翻转左子树,然后左右子树交换,再翻转左子树(原来右子树)
    public static TreeNode invertTree2(TreeNode root) {
        //递归出口
        if (root==null)return null;
        invertTree2(root.left);

        TreeNode tmp = root.left;
        root.left=root.right;
        root.right=tmp;
        invertTree2(root.left);
        return root;

    }
    //法三(后序遍历):先翻转左子树,再翻转右子树,然后左右子树交换
    public static TreeNode invertTree3(TreeNode root) {
        //递归出口
        if (root==null)return null;
        invertTree3(root.left);
        invertTree3(root.right);

        TreeNode tmp = root.left;
        root.left=root.right;
        root.right=tmp;

        return root;
    }

    //广度优先遍历
    public static TreeNode invertTree4(TreeNode root) {
        if (root==null)return null;

        Queue<TreeNode> queue =new LinkedList<>();
        queue.offer(root);
        while (queue.size()>0){
            //取出队首元素
            TreeNode node1 =queue.poll();
            //交换此节点的左右子树

            TreeNode tmp = node1.left;
            node1.left=node1.right;
            node1.right=tmp;
            //如果其有左孩子或者右孩子,将其存入队列中
            if (node1.left!=null) {
                queue.offer(node1.left);
            }
            if (node1.right!=null) {
                queue.offer(node1.right);
            }
        }
        return root;
    }
}

 

 

public class BuildTreeByOrder {

    public static void main(String[] args) {
        int[] preOrder = {1,2,3,4,5};
        int[] inOrder = {3,4,2,1,5};
        TreeNode root =buildTree(preOrder,inOrder);
    }

    /**
     *
     * @param preOrder  前序遍历结果
     * @param inOrder   中序遍历结果
     * @return
     */
    public static TreeNode buildTree(int[] preOrder,int[] inOrder){

        //递归出口
        if(preOrder.length==0)return null;
        //在前序结果中    找到根节点
        int rootValue = preOrder[0];
        TreeNode root = new TreeNode(rootValue);

        //查找根节点在中序结果中的位置
        int leftSize = find(inOrder,rootValue);

        //切分出左子树的前序和中序结果
        //使用 Arrays,copyOfRange方法 三个参数 原始数组 起始位置 终止位置
        //[起始位置,终止位置]
        //
        //2 3 4 [1,leftSize+1)
        //3 4 2 [0,leftSize)
        int[] leftPreOrder = Arrays.copyOfRange(preOrder,1,leftSize+1);
        int[] leftInOrder = Arrays.copyOfRange(inOrder,0,leftSize);
        root.left = buildTree(leftPreOrder,leftInOrder);

        //切分出右子树的前序和中序结果

        // 5 [leftSize+1,end)
        // 5 [leftSize+1,end)
        int[] rightPreOrder= Arrays.copyOfRange(preOrder,leftSize+1,preOrder.length);
        int[] rightInOrder= Arrays.copyOfRange(inOrder,leftSize+1,inOrder.length);
        root.right=buildTree(rightPreOrder,rightInOrder);
        return root;
    }

    //查找索引位置
    private static int find(int[] array,int value) {
        for (int i = 0; i < array.length; i++) {
            if (array[i]==value){
                return i;
            }
            
        }
        return -1;
    }
}

二叉查找树

二叉树的节点类

类名Node<Key,Value>
构造方法Node(Key key,Value value,Node left, Node right);创建Node对象
成员变量

1.public Node left; 记录左子节点

2.public Node right; 记录右子节点

3.public Key key; 存储键

4.public Value value;存储值

public class Node<Key, Value> {
    public Node left; //记录左子节点

    public Node right; //记录右子节点

    public Key key; //存储键

    public Value value;//存储值

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

查找树api

类名

BinaryTree<Key extends Comparable<Key>,Value >

构造方法BinaryTree:创建BinaryTree对象
成员变量

1.private Node root; 记录根节点

2.private int N; 记录树中元素的个数

成员方法

1.public void put(Key key,Value value); 向树中插入一个键值对

2.private Node put(Node x,Key key,Value val); 给定树x上,添加一个键值对,并返回添加后的新树

3.private Value get(Key key); 根据key,从树中找出对应的值

4.private Value get(Node x,Key key);从指定的数x中,找出key对应的值

5.public void delete(Key key);根据key,删除对应的键值对

6.private Node delete(Node x,Key key);删除指定树x上的键为key的键值对,并返回删除后的新树

7.public int size();获取树中元素的个数

插入方法 put实现思想:

1.如果当前树中没有任何一个点,则直接把新节点当做根节点使用

2.如果当前树不为空,则从根节点开始:

·        2.1如果新节点的key小于当前节点的key,则继续找当前节点的左子节点;

        2.2如果新节点的key大于当前节点的key,则继续找当前节点的右子节点;

        2.3如果新节点的key等于当前节点的key,则替换该节点的value值。

查询方法get实现思想:

从根节点开始:

        1.如果要查询的key小于当前节点的key,则继续找当前节点的左子节点;

        2.如果要查询的key大于当前节点的key,则继续找当前节点的右子节点;

        3.如果要查询的key等于当前节点的key,则返回当前节点的value。

删除方法delete实现思想:

        1.找到被删除的节点;

        2.找到被删除节点右子树中的最小节点minNode。

        3.删除右子树中的最小节点

        4.让被删除节点的左子树称为最小节点minNode的左子树,让被删除节点的右子树称为最小节点minNode的右子树

        5.让被删除节点的父节点指向最小节点minNode

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

    private Node root;//记录根节点

    private int N; //记录树中元素的个数

    public BinaryTree() {

    }

    public class Node {
        public Node left; //记录左子节点

        public Node right; //记录右子节点

        public Key key; //存储键

        public Value value;//存储值

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

        // 向树中插入一个键值对
        public void put(Key key, Value value) {
            root = put(root, key, value);
        }

        //给定树x上,添加一个键值对,并返回添加后的新树
        private Node put(Node x, Key key, Value val) {
            //如果x子树为空
            if (x == null) {
                N++;
                return new Node(null, null, key, val);
            }
            int cmp = key.compareTo(x.key);
            //如果新节点的key小于当前节点的key,则继续找当前节点的左子节点;
            if (cmp < 0) {
                x.left = put(x.left, key, val);
            }
            //如果新节点的key大于当前节点的key,则继续找当前节点的右子节点;
            if (cmp > 0) {
                x.right = put(x.right, key, val);
            }
            //如果新节点的key等于当前节点的key,则替换该节点的value值
            if (cmp == 0) {
                x.value = val;
            }
            return x;
        }

        //根据key,从树中找出对应的值
        public Value get(Key key) {
            return get(root, key);
        }

        //从指定的数x中,找出key对应的值
       private Value get(Node x, Key key) {
            //如果x子树为空
            if (x == null) {
                return null;
            }
            int cmp = key.compareTo(x.key);
            //如果要查询的key小于当前节点的key,则继续找当前节点的左子节点;
            if (cmp < 0) {
                return get(x.left, key);
            } else if (cmp > 0) {
                //如果新节点的key大于当前节点的key,则继续找当前节点的右子节点;
                return get(x.right, key);
            } else {
                //如果要查询的key等于当前节点的key,则返回当前节点的value。
                return x.value;
            }
        }

        //根据key,删除对应的键值对
        public void delete(Key key) {
            delete(root, key);
        }

        //删除指定树x上的键为key的键值对,并返回删除后的新树
        private Node delete(Node x, Key key) {
            //x树为null
            if (x == null) {
                return null;
            }
            int cmp = key.compareTo(x.key);
            //如果要删除的key小于当前节点的key,则继续找当前节点的左子节点;
            if (cmp < 0) {
                x.left = delete(x.left, key);
            } else if (cmp > 0) {
            //如果要删除的key大于当前节点的key,则继续找当前节点的右子节点;
                x.right = delete(x.right, key);

            } else {

                //让元素个数-1
                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) {
                        if (n.left.right!=null){
                            n.left = n.left.right;
                            n.left.right=null;
                        }else {
                            n.left=null;
                        }

                    } else {
                        n = n.left;
                    }
                }
//                //让x节点的左子树称为minNode的左子树
//                minNode.left = x.left;
//                //让x节点的右子树成为minNode的右子树
//                minNode.right = x.right;
//                //让x节点的父节点指向minNode
//                x = minNode;

                minNode.left = x.left;
                if (minNode == x.right) {
                    minNode.right = x.right.right;
                }else {
                    minNode.right = x.right;
                }
                x=minNode;
            }
            return x;
        }

        //获取树中元素的个数
        public int size() {
            return N;
        }
}
public class BinaryTreeTest {
    public static void main(String[] args) {
        BinaryTree<Integer, String> tree = new BinaryTree<>();
        //测试插入
        tree.put(20, "张三");
        tree.put(10, "李四");
        tree.put(25, "成一");
        tree.put(8, "王五");
        tree.put(16, "刘八");
        tree.put(23, "燕十四");
        tree.put(26, "方九");
        tree.put(6, "郭十");
        tree.put(9, "赵六");
        tree.put(14, "林二");
        tree.put(17, "孙零");

        System.out.println("插入完毕后元素的个数为:"+tree.size());
        tree.delete(10);
        System.out.println("删除键10后对应的元素是: " + tree.get(10));
       tree.delete(3);
        System.out.println("删除后键3对应的元素是: " + tree.get(3));
        tree.delete(20);
        System.out.println("删除后键3对应的元素是: " + tree.get(5));
        System.out.println("删除后元素的个数为:"+tree.size());
    }
}

 查找最小键

api

public Key min()找出树中最小的键
private Node min(Node x)找到指定树x中,最小键所在的节点

查找最大键

api

public Key max()找出树中的最大的键
public Node max(Node x)找出指定树x中,最大键所在的节点
   //查找树中最小的键
        public Key min(){
            return min(root).key;
        }
        private Node min(Node x){
        //判断x还有没有左子节点,如果有,则继续向左找,如果没有,则x就是最小键所在的节点
            if(x.left!=null){
                return min(x.left);
            }else {
                return x;
            }

        }
    //查找树中最大的键
    public Key max(){
        return max(root).key;
    }
    private Node max(Node x){
        //判断x还有没有右子节点,如果有,则继续向右找,如果没有,则x就是最大键所在的节点
        if(x.right!=null){
            return min(x.right);
        }else {
            return x;
        }
    }

层序遍历

 

  

 树的最大深度

package Tree.BinaryTree;

import java.util.LinkedList;
import java.util.Queue;

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

    private Node root;//记录根节点

    private int N; //记录树中元素的个数

    public BinaryTree() {

    }

    public class Node {
        public Node left; //记录左子节点

        public Node right; //记录右子节点

        public Key key; //存储键

        public Value value;//存储值

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

        // 向树中插入一个键值对
        public void put(Key key, Value value) {
            root = put(root, key, value);
        }

        //给定树x上,添加一个键值对,并返回添加后的新树
        private Node put(Node x, Key key, Value val) {
            //如果x子树为空
            if (x == null) {
                N++;
                return new Node(null, null, key, val);
            }
            int cmp = key.compareTo(x.key);
            //如果新节点的key小于当前节点的key,则继续找当前节点的左子节点;
            if (cmp < 0) {
                x.left = put(x.left, key, val);
            }
            //如果新节点的key大于当前节点的key,则继续找当前节点的右子节点;
            if (cmp > 0) {
                x.right = put(x.right, key, val);
            }
            //如果新节点的key等于当前节点的key,则替换该节点的value值
            if (cmp == 0) {
                x.value = val;
            }
            return x;
        }

        //根据key,从树中找出对应的值
        public Value get(Key key) {
            return get(root, key);
        }

        //从指定的数x中,找出key对应的值
        private Value get(Node x, Key key) {
            //如果x子树为空
            if (x == null) {
                return null;
            }
            int cmp = key.compareTo(x.key);
            //如果要查询的key小于当前节点的key,则继续找当前节点的左子节点;
            if (cmp < 0) {
                return get(x.left, key);
            } else if (cmp > 0) {
                //如果新节点的key大于当前节点的key,则继续找当前节点的右子节点;
                return get(x.right, key);
            } else {
                //如果要查询的key等于当前节点的key,则返回当前节点的value。
                return x.value;
            }
        }

        //根据key,删除对应的键值对
        public void delete(Key key) {
            delete(root, key);
        }

        //删除指定树x上的键为key的键值对,并返回删除后的新树
        private Node delete(Node x, Key key) {
            //x树为null
            if (x == null) {
                return null;
            }
            int cmp = key.compareTo(x.key);
            //如果要删除的key小于当前节点的key,则继续找当前节点的左子节点;
            if (cmp < 0) {
                x.left = delete(x.left, key);
            } else if (cmp > 0) {
            //如果要删除的key大于当前节点的key,则继续找当前节点的右子节点;
                x.right = delete(x.right, key);

            } else {

                //让元素个数-1
                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) {
                        if (n.left.right!=null){
                            n.left = n.left.right;
                            n.left.right=null;
                        }else {
                            n.left=null;
                        }

                    } else {
                        n = n.left;
                    }
                }
//                //让x节点的左子树称为minNode的左子树
//                minNode.left = x.left;
//                //让x节点的右子树成为minNode的右子树
//                minNode.right = x.right;
//                //让x节点的父节点指向minNode
//                x = minNode;

                minNode.left = x.left;
                if (minNode == x.right) {
                    minNode.right = x.right.right;
                }else {
                    minNode.right = x.right;
                }
                x=minNode;
            }
            return x;
        }

        //获取树中元素的个数
        public int size() {
            return N;
        }
        //查找树中最小的键
        public Key min(){
            return min(root).key;
        }
        private Node min(Node x){
        //判断x还有没有左子节点,如果有,则继续向左找,如果没有,则x就是最小键所在的节点
            if(x.left!=null){
                return min(x.left);
            }else {
                return x;
            }

        }
    //查找树中最大的键
    public Key max(){
        return max(root).key;
    }
    private Node max(Node x){
        //判断x还有没有右子节点,如果有,则继续向右找,如果没有,则x就是最大键所在的节点
        if(x.right!=null){
            return min(x.right);
        }else {
            return x;
        }
    }
    //前序遍历 获取树中所有的键
    public Queue<Key> preErgodic(){
        Queue<Key> keys = new LinkedList<>();
        preErgodic(root,keys);
        return keys;
    }
    //前序遍历,把指定树x中的所有键放入到keys队列中
    private void preErgodic(Node x,Queue<Key> keys){
        if (x==null){
            return;
        }
        //把x节点的所有键,并放到keys队列中
       keys.offer(x.key);
        //递归遍历x顶点的左子树
        if(x.left!=null){
            preErgodic(x.left,keys);
        }
        //递归遍历x节点的右子树
        if(x.right!=null){
            preErgodic(x.right,keys);
        }
    }

    //中序遍历 获取树中所有的键
    public Queue<Key> midErgodic(){
        Queue<Key> keys = new LinkedList<>();
        midErgodic(root,keys);
        return keys;
    }
    //中序遍历,把指定树x中的所有键放入到keys队列中
    private void midErgodic(Node x,Queue<Key> keys){
        if (x==null){
            return;
        }
        //递归遍历x顶点的左子树
        if(x.left!=null){
            midErgodic(x.left,keys);
        }
        //把x节点的所有键,并放到keys队列中
        keys.offer(x.key);
        //递归遍历x节点的右子树
        if(x.right!=null){
            midErgodic(x.right,keys);
        }
    }
    //后序遍历 获取树中所有的键
    public Queue<Key> afterErgodic(){
        Queue<Key> keys = new LinkedList<>();
        afterErgodic(root,keys);
        return keys;
    }
    //后序遍历,把指定树x中的所有键放入到keys队列中
    private void afterErgodic(Node x,Queue<Key> keys){
        if (x==null){
            return;
        }
        //递归遍历x顶点的左子树
        if(x.left!=null){
            afterErgodic(x.left,keys);
        }
        //递归遍历x节点的右子树
        if(x.right!=null){
            afterErgodic(x.right,keys);
        }
        //把x节点的所有键,并放到keys队列中
        keys.offer(x.key);
    }

    //使用层序遍历,获得整个树中所有的键
    public Queue<Key> layerErgodic(){
        //定义两个队列,分别存储树中的键和树中的节点
        Queue<Key> keys = new LinkedList<>();
        Queue<Node> nodes = new LinkedList<>();
        //默认,往队列中放入根节点
        nodes.offer(root);

        while (!nodes.isEmpty()){
            //从队列中弹出一个节点,把key放入到keys中
            Node n = nodes.poll();
            keys.offer(n.key);
            //判断当前节点还有没有左子节点,如果有,则放入到nodes中
            if(n.left!=null){
                nodes.offer(n.left);
            }
            //判断当前节点还有没有右子节点,如果有,则放入到nodes中
            if(n.right!=null){
                nodes.offer(n.right);
            }

        }
        return keys;
    }
    public int maxDepth(){
        return maxDepth(root);
    }
    //获取整个树的最大深度
    private int maxDepth(Node x){
        if (x==null){
            return 0;
        }
        //x的最大深度
        int max = 0;
        //左子树的最大深度
        int maxL = 0;
        //右子树的最大深度
        int maxR = 0;

        //计算x节点左子树的最大深度
        if(x.left!=null){
            maxL = maxDepth(x.left);
        }
        //计算x节点右子树的最大深度
        if(x.right!=null){
            maxR = maxDepth(x.right);
        }
        return maxL>maxR?maxL+1:maxR+1;
    }

}


折纸问题

 

public class PagerFoldingTest {
    public static void main(String[] args) {
        //模拟折纸过程,产生树
        Node<String> tree = createTree(3);
        //遍历树,打印每个节点
        printTree(tree);
    }

    //通过模拟对折N次纸,产生树
    public static Node<String> createTree(int N){
        //定义根节点
        Node<String> root = null;
        for (int i = 0; i < N; i++) {
            //1.当前是第一次对折
            if(i==0){
                root = new Node<>("down",null,null);
                continue;//下次循环
            }
            //2.当前不是第一次对折
            //定义一个辅助队列,通过层序遍历的思想,找到叶子节点,叶子节点添加子节点
            Queue<Node> queue = new LinkedList<>();
            queue.offer(root);

            //循环遍历队列
            while (!queue.isEmpty()){
                //从队列中弹出一个节点
                Node<String> tmp = queue.poll();
                //如果有左子节点,则把左子节点放入到队列中
                if (tmp.left!=null){
                    queue.offer(tmp.left);
                }
                //如果有右子节点,则把右子节点放入到队列中
                if (tmp.right!=null){
                    queue.offer(tmp.right);
                }

                //如果同时没有左子节点和右子节点,那么证明该节点是叶子节点,
                // 只需要给该节点添加左子节点和右子节点即可
                if(tmp.left==null&&tmp.right==null){
                    tmp.left = new Node<String>("down",null,null);
                    tmp.right = new Node<String>("up",null,null);
                }
            }
        }
        return root;
    }

    public static void printTree(Node<String> root){
        //需要使用中序遍历完成
        if (root==null){
            return;
        }
        //打印左子树的每一个节点
        if (root.left!=null){
            printTree(root.left);
        }
        //打印当前节点
        System.out.print(root.item+" ");
        //打印右子树的每一个节点
        if (root.right!=null){
            printTree(root.right);
        }
    }

    public static class Node<T> {
        public T item; //存储元素

        public Node left; //记录左子节点

        public Node right; //记录右子节点

        public Node(T item, Node left, Node right) {
            this.item = item;
            this.left = left;
            this.right = right;
        }
    }
}
down down up down down up up 

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

堆的特性:

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

        2.它通常由数组来实现,具体方法就是将二叉树的节点按照层级顺序放入数组中,根节点在位置1,它的子节点在位置2和3,二子节点的子节点分别在位置4,5,6和7,以此类推。

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

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

 API设计  

类名Heap<T extends Comparable<T>>
构造方法Heap(int capacity):创建容量为capacity的heap对象
成员方法

1.private boolean less(int i,int j); 判断堆中索引i处的元素是否小于索引j处的元素

2.private void exch(int i,int j); 交换堆中i索引和j索引处的值

3.public T delMax();删除堆中最大的元素,并返回这个最大元素

4.public void insert(T t);往堆中插入一个元素

5.private void swim(int k);使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置

6.private void sink(int k);使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置

成员变量

1.private T[] items;用来存储元素的数组

2.private int N;记录堆中元素的个数

package Tree.heap;

public class Heap<T extends Comparable<T>> {
    private T[] items;//用来存储元素的数组

    private int N;//记录堆中元素的个数

    //创建容量为capacity的heap对象
    public Heap(int capacity) {
        this.items = (T[]) new Comparable[capacity+1];//0索引废弃
        this.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 temp = items[i];
        items[i]=items[j];
        items[j]=temp;
    }

    //删除堆中最大的元素,并返回这个最大元素
    public T delMax() {
        T max =items[1];
        //交换索引1处的元素和最大索引处的元素,
        //让完全二叉树中最右侧的元素变为临时根节点
        exch(1,N);
        //最大索引处的元素删除掉
        items[N] = null;
        //元素个数-1
        N--;
        //通过下沉调整堆,让堆重新有序
        sink(1);
        return max;
    }

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

    //使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
    private void swim(int k) {
        //通过循环,不断的比较当前的节点的值和其父节点的值,
        // 如果发现父节点比当前值节点小,则交换位置

        while(k>1){
            //比较当前节点和父节点
            if(less(k/2,k)){
                exch(k/2,k);
            }
            k=k/2;
        }
    }

    //使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
    private void sink(int k) {
        //通过循环不断地对比当前k节点和其左子节点2*k以及右子节点2k+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;
            }
            //交换k索引处的值和max索引处的值
            exch(k,max);

            //交换k的值
            k=max;

        }

    }
}
public class HeapTest {
    public static void main(String[] args) {
        //创建堆对象
        Heap<String> heap=new Heap<>(10);
        //在堆中存入字符串数组
        heap.insert("A");
        heap.insert("B");
        heap.insert("C");
        heap.insert("D");
        heap.insert("E");
        heap.insert("F");
        heap.insert("G");

        //通过循序从堆中删除数据
        String result = null;
        while ((result= heap.delMax())!=null){
            System.out.print(result+" ");
        }
    }
}

console:
G F E D C B A 

 堆排序

   给定一个数组:

        String[] arr = {"S","O","R","T","E","X","A","M","P","L","E"}

请对数组中的字符从小到大排序。

API设计

类名HeapSort<T extends Comparable<T>>
成员方法

1.public static void sort(Comparable[] source); 对source数组中的数据从小到大排序

2.private static void createHeap(Comparable[] source,Comparable[] heap);根据原数组source,构造出堆heap

3.private static boolean less(Comparable[] heap,int i, int j); 判断heap堆中索引i处的元素是否小于索引j处的元素

4.private static void exch(Comparable[] heap,int i, int j); 交换heap堆中i索引和j索引处的值

5.private static void sink(Comparable[] heap,int target, int range);在heap堆中,对target处的元素做下沉,范围是0-range。

public class HeapSort<T extends Comparable<T>> {

    //对source数组中的数据从小到大排序
    public static void sort(Comparable[] source) {
        //构建堆
        Comparable[] heap = new Comparable[source.length+1];
        createHeap(source,heap);
        //定义一个变量,记录未排序的元素中最大的索引
        int N = heap.length-1;
        //通过循环,交换1索引处的元素和排序的元素中最大索引处的元素
        while (N!=1){
            //交换元素
            exch(heap,1,N);
            //排序交换后最大元素所在的索引,让它不要参与堆的下沉调整
            N--;
            //需要对索引1处的元素进行堆的下沉调整
            sink(heap,1,N);
        }
        //把heap中的数据复制到原数组source中
        System.arraycopy(heap,1,source,0,source.length);
    }
    //根据原数组source,构造出堆heap
    private static void createHeap(Comparable[] source, Comparable[] heap) {
        //把source中的元素拷贝到heap中,heap中的元素就形成一个无序的堆
        System.arraycopy(source,0,heap,1,source.length);
        //对堆中的元素做下沉调整(从长度的一半开始,往索引1处扫描)
        for (int i =  heap.length/2;i>0 ;i--) {
          sink(heap,i,heap.length-1);
        }

    }

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

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

    //在heap堆中,对target处的元素做下沉,范围是0-range。
    private static void sink(Comparable[] heap, int target, int 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;
            }
            //2.比较当前节点的值和较大子节点的值
            if(!less(heap,target,max)){
                break;
            }
            exch(heap,target,max);
            target = max;
        }
    }
}
public class HeapSortTest {
    public static void main(String[] args) {
        //待排序数据
        String[] arr = {"S","O","R","T","E","X","A","M","P","L","E"};
        //通过heapSort对数组排序
        HeapSort.sort(arr);
        //打印排序后的数组元素
        System.out.println(Arrays.toString(arr));
    }
}

console:
[A, E, E, L, M, O, P, R, S, T, X]

优先队列

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

        

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

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

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

 最大优先队列

        基于堆区实现最大优先队列 

最大优先队列API设计

类名MaxPriorityQueue<T extends Comparable<T>>
构造方法MaxPriorityQueue(int capacity); 创建容量为capacity的MaxPriorityQueue对象
成员方法

1.private boolean less(int i,int j) 判断堆中i索引处的元素是否小于j索引处的元素

2.private void exch(int i,int j) 交换堆中i索引和j索引处的值

3.public T delMax() 删除队列中最大的元素,并返回这个最大元素

4.public void insert(T t) 往队列中插入一个元素

5.private void swim(int k) 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置

6.private void sink(int k) 使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置

7.public int size() 获取队列中元素的个数

8.public boolean isEmpty() 判断队列是否为空

成员变量

1.private T[] items; 用来存储元素的数组

2.private int N; 记录堆中元素的个数

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;
    }

    //判断堆中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 temp = items[i];
        items[i]=items[j];
        items[j]= temp;
    }

    //删除队列中最大的元素,并返回这个最大元素
    public T delMax() {
        T max =items[1];
        //交换索引1处的元素和最大索引处的元素,
        //让完全二叉树中最右侧的元素变为临时根节点
        exch(1,N);
        //最大索引处的元素删除掉
        items[N] = null;
        //元素个数-1
        N--;
        //通过下沉调整堆,让堆重新有序
        sink(1);
        return max;

    }

    //往队列中插入一个元素
    public void insert(T t) {
        items[++N]=t;//索引从1开始
        swim(N);
    }

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

    //使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
    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重置
            k=max;
        }
    }

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

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

public class MaxTest {
    public static void main(String[] args) {
        MaxPriorityQueue<String> queue = new MaxPriorityQueue<>(10);
        //往队列中存储元素
        queue.insert("A");
        queue.insert("B");
        queue.insert("C");
        queue.insert("E");
        queue.insert("F");
        queue.insert("G");

        //通过循环从队列中获取最大的元素
        while(!queue.isEmpty()){
            String max = queue.delMax();
            System.out.print(max+" ");
        }

    }
}

console:
G F E C B A 

 最小优先队列

之前的堆:最大堆

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

        2.每个节点的数据总是大于等于它的两个子节点的数据。

与之相反的最小堆        

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

        2.每个节点的数据总是小于等于它的两个子节点的数据。

 API设计

类名MinPriorityQueue<T extends Comparable<T>>
构造方法MinPriorityQueue(int capacity); 创建容量为capacity的MinPriorityQueue对象
成员方法

1.private boolean less(int i,int j) 判断堆中i索引处的元素是否小于j索引处的元素

2.private void exch(int i,int j) 交换堆中i索引和j索引处的值

3.public T delMin() 删除队列中最小的元素,并返回这个最小元素

4.public void insert(T t) 往队列中插入一个元素

5.private void swim(int k) 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置

6.private void sink(int k) 使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置

7.public int size() 获取队列中元素的个数

8.public boolean isEmpty() 判断队列是否为空

成员变量

1.private T[] items; 用来存储元素的数组

2.private int N; 记录堆中元素的个数

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;
    }

    //判断堆中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 temp = items[i];
        items[i]=items[j];
        items[j]= temp;
    }

    //删除队列中最小的元素,并返回这个最小元素
    public T delMin() {
        T min =items[1];
        //交换索引1处的元素和最大索引处的元素,
        //让完全二叉树中最右侧的元素变为临时根节点
        exch(1,N);
        //最大索引处的元素删除掉
        items[N] = null;
        //元素个数-1
        N--;
        //通过下沉调整堆,让堆重新有序
        sink(1);
        return min;

    }

    //往队列中插入一个元素
    public void insert(T t) {
        items[++N]=t;//索引从1开始
        swim(N);
    }

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

    //使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
    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重置
            k=min;
        }
    }

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

    //判断队列是否为空
    public boolean isEmpty() {
        return N==0;
    }
}
public class MinTest {
    public static void main(String[] args) {
        MinPriorityQueue<String> queue = new MinPriorityQueue<>(10);
        //往队列中存储元素
        queue.insert("G");
        queue.insert("F");
        queue.insert("E");
        queue.insert("D");
        queue.insert("C");
        queue.insert("B");
        queue.insert("A");

        //通过循环从队列中获取最大的元素
        while(!queue.isEmpty()){
            String min = queue.delMin();
            System.out.print(min+" ");
        }

    }
}
console:
A B C D E F G 

 索引优先队列

        最大和最小优先队列,可以访问最大和最小元素,但无法通过索引访问已存在的对象,并更新它们。索引优先队列就是实现这个目的的。

索引优先队列实现思路

 

 API设计

类名IndexMinPriorityQueue<T extends Comparable<T>>
构造方法IndexMinPriorityQueue(int capacity); 创建容量为capacity的IndexMinPriorityQueue对象
成员方法

1.private boolean less(int i,int j) 判断堆中i索引处的元素是否小于j索引处的元素

2.private void exch(int i,int j) 交换堆中i索引和j索引处的值

3.public T delMin() 删除队列中最小的元素,并返回这个最小元素

4.public void insert(int i,T t) 往队列中插入一个元素,并关联索引

5.private void swim(int k) 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置

6.private void sink(int k) 使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置

7.public int size() 获取队列中元素的个数

8.public boolean isEmpty() 判断队列是否为空

9.public boolean contains(int k) 判断k对应的元素是否存在

10.public void changeItem(int i,T t) 把与索引i相关联的元素修改为t

11.public int minIndex() 最小元素相关联的索引

12.public void delete(int i) 删除索引i相关联的元素

成员变量

1.private T[] items; 用来存储元素的数组

2.private int N; 记录堆中元素的个数

3.private int[] pq; 保存每个元素在items数组中的索引,pq数组需要堆有序

4.private int[] qp; pq的逆序,pq的值作为索引,pq的索引作为值

public class IndexMinPriorityQueue<T extends Comparable<T>> {
    private T[] items; //用来存储元素的数组

    private int N; //记录堆中元素的个数

    private int[] pq; //保存每个元素在items数组中的索引,pq数组需要堆有序

    private int[] qp; //pq的逆序,pq的值作为索引,pq的索引作为值

    public IndexMinPriorityQueue(int capacity) {
         this.items= (T[]) new Comparable[capacity+1];
         this.pq=new int[capacity+1];
         this.qp=new int[capacity+1];
         this.N=0;
        for (int i = 0; i < qp.length; i++) {
            qp[i]=-1;
        }
    }

    //判断堆中i索引和j索引处的值
    private boolean less(int i, int j) {
        return items[pq[i]].compareTo(items[pq[j]])<0;
    }

    //交换堆中i索引和j索引处的值
    private void exch(int i, int j) {
        //交换pq中的数据
        int temp = pq[i];
        pq[i]=pq[j];
        pq[j]= temp;

        //更新qp中的数据
        qp[pq[i]]=i;
        qp[pq[j]]=j;
    }

    //删除队列中最小的元素,并返回这个最小元素关联的索引
    public int delMin() {
        //获取最小元素关联的索引
        int minIndex=pq[1];

        //交换pq中索引1处的元素和最大索引处的元素,
        //让完全二叉树中最右侧的元素变为临时根节点
        exch(1,N);
        //删除qp中对应的内容
        qp[pq[N]]=-1;
        //删除items中对应的元素
        items[minIndex]=null;

        //元素个数-1
        N--;
        //通过下沉调整堆,让堆重新有序
        sink(1);
        return minIndex;

    }

    //往队列中插入一个元素,并关联索引
    public void insert(int i,T t) {
        //判断i是否已经被关联,如果已经被关联,则不让插入
        if (contains(i)){
            return;
        }
        //元素个数+1
        N++;
        //把数据存储到items对应的
        items[i]=t;
        //把i存储到pq中
        pq[N]=i;
        //通过qp来记录pq中的i
        qp[i]=N;
        //通过堆上浮完成堆的调整
        swim(N);
    }

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

    //使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
    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重置
            k=min;
        }
    }

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

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

    //判断k对应的元素是否存在
    public boolean contains(int k){
        return qp[k]!=-1;

    }
    //把与索引i相关联的元素修改为t
    public void changeItem(int i,T t){
        //修改items数组中i位置的元素为t
        items[i]=t;
        //找到i在pq中出现的位置
        int k = pq[i];
        //堆调整
        sink(k);
        swim(k);


    }
    //最小元素相关联的索引
    public int minIndex(){

        return pq[1];

    }
    //删除索引i相关联的元素
    public void delete(int i){
        //找到i在pq中的索引
        int k = qp[i];
        //交换pq中索引k处和索引N处的值
        exch(k,N);
        //删除qp中的内容
        qp[pq[N]]=-1;
        //删除pq中的内容
        pq[N]=-1;
        //删除items中的内容
        items[k]=null;
        //元素数量减-1
        N--;
        //堆的调整
        sink(k);
        swim(k);
    }

    @Override
    public String toString() {
        StringBuilder s = new StringBuilder();
        int x=0 ;
        while(items[x]!=null){
            x++;
        }
        for (int i = 1; i <x+1; i++) {
            s.append(items[pq[i]]+" ");
        }
        s.deleteCharAt(s.length()-1);
        return s.toString();

    }
}
public class IndexMinTest {
    public static void main(String[] args) {
        IndexMinPriorityQueue<String> queue = new IndexMinPriorityQueue<>(10);
        //往队列中存储元素
        queue.insert(0,"A");
        queue.insert(1,"C");
        queue.insert(2,"F");
        System.out.println(queue);
        queue.changeItem(2,"B");
        while (!queue.isEmpty()){
            int index=queue.delMin();
            System.out.print(index+" ");
        }
        
    }
}

console
A F C
0 2 1

平衡树

遇到查找不平衡的斜树类型时,容易受插入数据影响,有没有种方法,让生成的树都向完全二叉树那样,那么即使在最坏的情况下,查找的效率依旧会很好

2-3查找树

为了保证查找树的平衡性,我们需要一些灵活性,因此在这里我们允许树的一个节点保存多个键。确切的说,我们将一棵标准的二叉查找树中的节点称为2-节点(含有一个键和两条链)。而现在我们引入3-节点,它含有两个键和3条链。-节点和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树是自底向上生长。

红黑树

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

        红链接:将两个2-节点连接起来构成一个3-节点;

        黑链接:则是2-3树中的普通链接。

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

 红黑树的定义

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

1.红链接均为左链接;

2.没有任何一个节点同时和两条红链接相连;

3.该树是完美黑色平衡的,即在任意空链接到根节点的路径上的黑链接数量相同;

下面是红黑树与2-3树的对应关系:

 红黑树节点API设计

类名Node<Key,Value>
构造方法Node(Key key, Value value,Node left,Node right,boolean color):创建Node对象
成员变量

1.public Node left; 记录左子节点

2.public Node right; 记录右子节点

3.public Key key; 存储键

4.public Value value;存储值

5.public boolean color; 由其父节点指向它的链接的颜色

 平衡化

       在对红黑树进行一些增删改查的操作后,很有可能会出现红色的右链接或者两条连续红色的链接,而这些都不满足红黑树的定义,所以需要对这种情况通过旋转进行修复,让红黑树保持平衡。

左旋 

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

前提:当前节点为h,它的右子节点为x;

左旋过程:

        1.让x的左子节点为变为h的右子节点;h.right=x.left;

        2.让h成为x的左子节点:x.left=h;

        3.让h的color属性变为x的color属性值: x.color=h.color;

        4.让h的color属性变为RED:h.color=true;

右旋 

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

前提:当前节点为h,它的左子节点为x;

右旋过程:

        1.让x的右子节点成为h的左子节点:h.left = x.right;

        2.让h成为x的右子节点: x.right = h;

        3.让x的color变为h的color属性值:x.color = h.color;

        4.让h的color为RED;

插入

  

 

 

红黑树的API设计 

  

类名RedBlackTree<Key extends Comparable<Key>,Value>
构造方法ReaBlack Tree(); 创建RedBlackTree 对象
成员方法

1.private boolean isRed(Node x); 判断当前节点的父指向链接是否为红色

2.private Node rotateLeft(Node h); 左旋调整

3.private Node rotateRight(Node h); 右旋调整

4.private void flipColors(Node h); 颜色反转,相当于完成拆分4-节点

5.public void put(Key key,value val); 在整个树上完成插入操作

6.private Node put(Node h,Key key,Value val); 在指定树中,完成插入操作,并返回添加元素后新的树

7.public Value get(Key key); 根据key,从树中找出对应的值

8.private Value get(Node x,Key key);从指定的树x中,找出key对应的值

9.public int size(); 获取树中元素的个数

成员变量

1.private Node root; 记录根节点

2.private int N; 记录树中元素的个数

3.private static final boolean RED; 红色链接标识

4.private static final boolean BLACK;黑色链接标识

package Tree.RedBlack;

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;//黑色链接标识

    public RedBlackTree() {

    }
    public class Node {

        public Node left; //记录左子节点

        public Node right;// 记录右子节点

        public Key key; //存储键

        public Value value;//存储值

        public boolean color;// 由其父节点指向它的链接的颜色
        //创建Node对象
        public Node(Key key, Value value,Node left, Node right,  boolean color) {
            this.left = left;
            this.right = right;
            this.key = key;
            this.value = value;
            this.color = color;
        }
    }

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

    //左旋调整
    private Node rotateLeft(Node h) {
        //获取h节点的右子节点,表示为x
        Node x =h.right;
        //让x节点的左子节点称为h节点的右子节点
        h.right = x.left;
        //让h成为x节点的左子节点
        x.left = h;
        //让x节点的color属性等于h节点的color属性
        x.color = h.color;
        //让h节点的color属性变为红色
        h.color=RED;
        return x;

    }

    //右旋调整
    private Node rotateRight(Node h) {
        //获取h节点的左子节点,表示为x
        Node x =h.left;
        //让x节点的右子节点称为h节点的左子节点
        h.left = x.right;
        //让h成为x节点的右子节点
        x.left = h;
        //让x节点的color属性等于h节点的color属性
        x.color = h.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;

    }

    //在指定树中,完成插入操作,并返回添加元素后新的树
    private Node put(Node h, Key key, Value val) {
        //判断h是否为空,如果为空则直接返回一个红色的节点就可
        if(h==null){
            N++;//数量+1
            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.left)&&isRed(h.right)){
           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;
    }

    //根据key,从树中找出对应的值
    public Value get(Key key) {
        return get(root,key);
    }

    //从指定的树x中,找出key对应的值
    private Value get(Node x, Key key) {
        if (x==null) return null;

        //比较x节点的键和key的大小
        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;
        }
    }

    //获取树中元素的个数
    public int size() {
        return N;
    }
}
public class RedBlackTest {
    public static void main(String[] args) {

        RedBlackTree<String, String> tree = new RedBlackTree<>();

        tree.put("1","张三");
        tree.put("2","李四");
        tree.put("3","王五");

        String r1 = tree.get("1");
        System.out.println(r1);
        String r2 = tree.get("2");
        System.out.println(r2);
        String r3 = tree.get("3");
        System.out.println(r3);
        
    }
}


张三
李四
王五

B-树

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

B树的特性

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

  • 每一个节点最多有M-1个key,并且以升序排列;
  • 每个节点最多能有M个子节点;
  • 根节点至少有两个子节点;

B树中插入数据 

 

B+树

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

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

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

 B+树存储数据

 

 B+树和B树对比

B+树的优点

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

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

B树的优点

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

B+树在数据库中的应用

        在数据库操作中,查询操作可以说是最频繁的一种2操作,因此在设计数据库时,必须要考虑到查询的效率问题,在很多数据库中,都是用到了B+树来提高查询的效率。

        查询18只需比较3次

  •         

 并查集

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

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

 并查集结构

并查集也是一种树型结构,但这课树要求比较简单:

1.每个元素都唯一的对应一个节点;

2.每一组数据中的多个元素都在同一棵树中;

3.一个组中的数据对应的树和另外一个组的数据对应的树之间没有任何联系;

4.元素在树中并没有子父级关系的硬性要求;

 API设计

类名UF
构造方法UF(int N); 初始化并查集,以整数标识(0,N-1)个节点
成员方法

1.public int count()  获取当前并查集中的数据有多少个分组

2.public boolean connected(int p,int q); 判断并查集中元素p和q是否在同一分组中

3.public int find(int p); 元素p所在分组的标识符

4.public void union(int p, int q)  判断并查集中元素所在分组合并

成员变量

1.private int[] eleAndGroup  记录节点元素和该元素所在分组的标识

2.private int count  记录并查集中数据的分组个数

 

//并查集
public class UF {

    public static void main(String[] args) {

        //创建并查找对象
        UF uf = new UF(5);
        System.out.println("默认情况下,并查集中有:"+uf.count()+"分组");

        //从控制台录入两个要合并的元素,调用union方法合并,
        // 观察并查集中的分组是否减少
        Scanner sc = new Scanner(System.in);

        while (true){
            System.out.println("请输入第一个要合并的元素:");
            int p = sc.nextInt();
            System.out.println("请输入第二个要合并的元素:");
            int q = sc.nextInt();

            //判断这两个元素是否已经在同一组了
            if (uf.connected(p,q)){
                System.out.println(p+"元素"+q+"元素已经在同一个组中了");
                continue;
            }
            uf.union(p,q);
            System.out.println("当前并查集中还有:"+uf.count()+"个分组");

        }

    }
    private int[] eleAndGroup;// 记录节点元素和该元素所在分组的标识

    private int count;//记录并查集中数据的分组个数


    public UF(int N) {
        //初始化分组数量
        this.count=N;
        //初始化eleAndGroup数组
        this.eleAndGroup = new int[N];
        //初始化elaAndGroup中的元素及其所在的组的标识符,
        // 让eleAndGroup数组的索引作为并查集的每一个节点的元素,
        // 并且让每个索引处的值(该元素所在组的标识)就是该索引
        for (int i = 0; i < eleAndGroup.length; i++) {
            eleAndGroup[i]=i;
        }
    }

    //获取当前并查集中的数据有多少个分组
    public int count() {
        return count;
    }

    // 判断并查集中元素p和q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }

    //元素p所在分组的标识符
    public int find(int p){
        return eleAndGroup[p];
    }

    //判断并查集中元素所在分组合并
    public void union(int p, int q) {
       //判断元素q和p是否已经在同一分组中,如果一斤在同一分组中,则结束方法就可以了
        if(connected(p,q)){
            return;
        }

        //找到p所在分组的标识符
        int pGroup = find(p);

        //找到q所在分组的标识符
        int qGoup = find(q);

        //合并组,让p所在组的所有元素的组标识符变为q所在分组的标识符
        for (int i = 0; i < eleAndGroup.length; i++) {
            if (eleAndGroup[i]==pGroup) {
                eleAndGroup[i]=qGoup;
            }
        }
        //分组个数-1
        this.count--;
    }
}

 该合并n个元素需要执行n-1次方法

并查集算法优化

API设计 

类名UF_Tree
构造方法UF_Tree(int N); 初始化并查集,以整数标识(0,N-1)个节点
成员方法

1.public int count()  获取当前并查集中的数据有多少个分组

2.public boolean connected(int p,int q); 判断并查集中元素p和q是否在同一分组中

3.public int find(int p); 元素p所在分组的标识符

4.public void union(int p, int q)  判断并查集中元素所在分组合并

成员变量

1.private int[] eleAndGroup  记录节点元素和该元素的父节点

2.private int count  记录并查集中数据的分组个数

find(int p)查询方法实现 

1.判断当前元素p的父节点eleAndGroup[p]是不是自己,如果是自己则证明已经是根节点了;

2.如果当前元素p的父节点不是自己,则让p=eleAndGroup[p],继续找父节点的父节点,直到找到根节点为止;

union(intp,int q)方法实现

1.找到p元素所在的根节点

2.找到q元素所在树的根节点

3.如果p和q已经在同一树中,则无需合并;

4.如果p和q不在同一个分组,则只需要将p元素所在树根节点的父节点设置为q元素的根节点即可;

5.分组数量-1

//并查集树
public class UF_Tree {

    public static void main(String[] args) {

        //创建并查找对象
        UF_Tree uf = new UF_Tree(5);
        System.out.println("默认情况下,并查集中有:"+uf.count()+"分组");

        //从控制台录入两个要合并的元素,调用union方法合并,
        // 观察并查集中的分组是否减少
        Scanner sc = new Scanner(System.in);

        while (true){
            System.out.println("请输入第一个要合并的元素:");
            int p = sc.nextInt();
            System.out.println("请输入第二个要合并的元素:");
            int q = sc.nextInt();

            //判断这两个元素是否已经在同一组了
            if (uf.connected(p,q)){
                System.out.println(p+"元素"+q+"元素已经在同一个组中了");
                continue;
            }
            uf.union(p,q);
            System.out.println("当前并查集中还有:"+uf.count()+"个分组");

        }

    }
    private int[] eleAndGroup;// 记录节点元素和该元素的父节点

    private int count;//记录并查集中数据的分组个数


    public UF_Tree(int N) {
        //初始化分组数量
        this.count=N;
        //初始化eleAndGroup数组
        this.eleAndGroup = new int[N];
        //初始化elaAndGroup中的元素及其所在的组的标识符,
        // 让eleAndGroup数组的索引作为并查集的每一个节点的元素,
        // 并且让每个索引处的值(该元素所在组的标识)就是该索引
        for (int i = 0; i < eleAndGroup.length; i++) {
            eleAndGroup[i]=i;
        }
    }

    //获取当前并查集中的数据有多少个分组
    public int count() {
        return count;
    }

    // 判断并查集中元素p和q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }

    //元素p所在分组的标识符
    public int find(int p){
        while (true){
            if (p==eleAndGroup[p]){
                return p;
            }
            p= eleAndGroup[p];
        }

    }

    //判断并查集中元素所在分组合并
    public void union(int p, int q) {
       //找到p元素和q元素所在的组对应树的根节点

        int pRoot = find(p);
        int qRoot = find(q);
        //如果p所在的树的根节点的父节点为q所在树的根节点即可
        eleAndGroup[pRoot]=qRoot;

        //组的数量-1
        this.count--;
    }
}

 路径压缩

 UF_Tree中最坏情况下union算法的时间复杂度为O(N^2),其最主要的问题在于最坏情况下,树的深度和数组的大小一样,如果我们能够通过一些算法让合并时,生成的树的深度尽可能的小,就可以优化find方法。

之前我们在union算法中,合并树的时候将任意一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果我们把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度。

 只要我们保证每次合并,都能把小树合并到大树上,就能够压缩合并后新树的路径,这样就能提高find方法的效率。为了完成这个需求,我们需要另外一个数组来记录存储每个根节点对应的树中元素的个数,并且需要一些代码调整数组中的值。

UF_Tree_Weighted API设计

类名UF_Tree_Weighted
构造方法UF_Tree_Weighted(int N); 初始化并查集,以整数标识(0,N-1)个节点
成员方法

1.public int count()  获取当前并查集中的数据有多少个分组

2.public boolean connected(int p,int q); 判断并查集中元素p和q是否在同一分组中

3.public int find(int p); 元素p所在分组的标识符

4.public void union(int p, int q)  判断并查集中元素所在分组合并

成员变量

1.private int[] eleAndGroup  记录节点元素和该元素的父节点

2.private int count  记录并查集中数据的分组个数

3.private int[] sz;  存储每个根节点对应的树中元素的个数

//并查集树
public class UF_Tree_Weight {

    public static void main(String[] args) {

        //创建并查找对象
        UF_Tree_Weight uf = new UF_Tree_Weight(5);
        System.out.println("默认情况下,并查集中有:"+uf.count()+"分组");

        //从控制台录入两个要合并的元素,调用union方法合并,
        // 观察并查集中的分组是否减少
        Scanner sc = new Scanner(System.in);

        while (true){
            System.out.println("请输入第一个要合并的元素:");
            int p = sc.nextInt();
            System.out.println("请输入第二个要合并的元素:");
            int q = sc.nextInt();

            //判断这两个元素是否已经在同一组了
            if (uf.connected(p,q)){
                System.out.println(p+"元素"+q+"元素已经在同一个组中了");
                continue;
            }
            uf.union(p,q);
            System.out.println("当前并查集中还有:"+uf.count()+"个分组");

        }

    }
    private int[] eleAndGroup;// 记录节点元素和该元素的父节点

    private int count;//记录并查集中数据的分组个数

    private int[] sz;// 存储每个根节点对应的树中元素的个数

    public UF_Tree_Weight(int N) {
        //初始化分组数量
        this.count=N;
        //初始化eleAndGroup数组
        this.eleAndGroup = new int[N];
        //初始化elaAndGroup中的元素及其所在的组的标识符,
        // 让eleAndGroup数组的索引作为并查集的每一个节点的元素,
        // 并且让每个索引处的值(该元素所在组的标识)就是该索引
        for (int i = 0; i < eleAndGroup.length; i++) {
            eleAndGroup[i]=i;
        }
        //默认情况下,sz数组每个索引值都是1
        this.sz=new  int[N];
        for (int i = 0; i < sz.length; i++) {
            sz[i]=1;
        }
    }

    //获取当前并查集中的数据有多少个分组
    public int count() {
        return count;
    }

    // 判断并查集中元素p和q是否在同一分组中
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }

    //元素p所在分组的标识符
    public int find(int p){
        while (true){
            if (p==eleAndGroup[p]){
                return p;
            }
            p= eleAndGroup[p];
        }

    }

    //判断并查集中元素所在分组合并
    public void union(int p, int q) {
       //找到p元素和q元素所在的组对应树的根节点

        int pRoot = find(p);
        int qRoot = find(q);

        if (pRoot==qRoot){
            return;
        }

        //判断proot对应的树大还是qroot对应的树大,最终需要把较小的树合并到较大的树中
        if (sz[pRoot]<sz[qRoot]){
            eleAndGroup[pRoot]=qRoot;
            sz[qRoot]+=sz[pRoot];
        }else {
            eleAndGroup[qRoot]=pRoot;
            sz[pRoot]+=sz[qRoot];
        }

//        //如果p所在的树的根节点的父节点为q所在树的根节点即可
//        eleAndGroup[pRoot]=qRoot;

        //组的数量-1
        this.count--;
    }
}
默认情况下,并查集中有:5分组
请输入第一个要合并的元素:
1
请输入第二个要合并的元素:
2
当前并查集中还有:4个分组
请输入第一个要合并的元素:

 案例:畅通工程

public class Traffic_Project_Test {
    public static void main(String[] args) throws IOException {
        //构建缓冲读取流
        BufferedReader br = new BufferedReader
                (new InputStreamReader
                        (Traffic_Project_Test.class.getResourceAsStream("traffic_project.txt")));

        //读取第一行数据
        int i = Integer.parseInt(br.readLine());
        //创建一个并查集对象
        UF_Tree_Weight uf = new UF_Tree_Weight(i);
        //读取第二行数据
        int i1 = Integer.parseInt(br.readLine());
        //循环读取7条数据
        for (int i2 = 0; i2 < i1; i2++) {
            String line = br.readLine();
            String[] s = line.split(" ");
            int p = Integer.parseInt(s[0]);
            int q = Integer.parseInt(s[1]);
            //调用并查集对象让两城市相通
            uf.union(p,q);

        }
        //获取并查集剩余数量-1就可以得到还需修建道路的数量
        int roads = uf.count()-1;
        System.out.println("还需修建道路数量为:"+roads);
    }
}


还需修建道路数量为:12

图 

基础知识

图是由顶点集合和顶点之间边的集合组成 ,通常表示为G-V-E    G(V,E)

G 表示图          V表示图中顶点的集合        E表示图中边的集合

特殊的图:1,自环:即一条连接一个顶点和其自身的边;

2.平行边:连接同一对顶点的两条边;

图的分类

根据图中边的方向可分为:无向图和有向图

无向图:顶点之间没有指向       

有向图:顶点之间有方向 

带权无向图:顶点之间的边加上了权重的度量    

带权有向图:顶点之间的边加上了权重的度量 (注意:两顶点之间不同方向之间的权重可以不同)

无向图

 图的相关术语

相邻顶点:

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

度:

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

子图:

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

路径:

        是由边顺序连接的一系列的顶点组成

环:

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

连通图:

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

连通子图:

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

图的存储结构

常用的图的表示结构:邻接矩阵和邻接表

邻接矩阵

下面四种图

其临接矩阵为: 

 

 邻接表

图的实现

 图的API设计

类名Graph
构造方法Graph(int V):创建一个包含v个顶点但不包含边的图
成员方法

1.public int V():获取图中顶点的数量

2.publi int E():获取图中边的数量

3.public void addEdge(int V,int W):向图中添加一条边v-w

4.public Queue<Integer>adj(int v):获取和顶点v相邻的所有顶点     

成员变量

1.private final int V:记录顶点数量

2.private int E:记录边的数量

3.private Queue <Integer>[]adj:邻接表

import java.util.LinkedList;
import java.util.Queue;

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

    public Graph(int V){
        //初始化顶点数量
        this.V=V;
        //初始化边的数量
        this.E=0;
        //初始化邻接表
        this.adj=new Queue[V];
        for (int i = 0; i < adj.length; i++) {
            adj[i] = new LinkedList<>();


        }
    }

    //获取顶点数目
    public int V(){
        return V;
    }

    //获取边的数目
    public int E(){
        return E;
    }

    //向图中添加一条边  v-w
    public void addEdge(int v,int w){
        //在无向图,边是没有方向,所以改变既可以是(v到w的边,又可以从w到v的边,
        // 因此需要让w出现在v的;邻接表中,并且还要让v出现在w的邻接表中)
        adj[v].offer(w);
        adj[w].offer(v);
        //边的数量+1
        E++;
    }

    //获取和顶点v相邻的所有顶点
    public Queue<Integer> adj(int v){
        return adj[v];
    }
}

图的搜索

深度优先搜索

         因为边是没有方向的,为了不对顶点重复搜索,应有标记来表示当前有没有搜索过,可以使用一个布尔类型的数组        boolean[V] marked,索引代表顶点,值代表当前顶点是否已经搜索,如果已经搜索,标记为true,如果没有搜索,标记为false;

类名DepthFirstSearch
构造方法DepthFirstSearch(Graph G,int s):构造深度优先搜索对象,使用深度优先搜索找到G图s顶点的所有相通顶点
成员方法

1.private void dfs(Graph G,int v):使用深度优先搜索找出G图中v顶点的所有相通的顶点

2.public boolean marked(int w):判断w顶点与s顶点是否相通

3.public int count(): 获取与顶点s相通的所有顶点的总数

成员变量

1.private boolean[] marked: 索引代表顶点,值代表当前顶点是否已经被搜索

2.private int count:记录有多个顶点与s顶点相通

//深度优先搜索
public class DepthFirstSearch {
    //索引代表顶点,值表示当前顶点是否已经被搜索
    private boolean[] marked;
    //记录有多少个顶点与s顶点相通
    private int count;
    //构造深度优先搜索对象,使用深度优先搜索找出s顶点的所有相邻顶点
    public DepthFirstSearch(Graph G,int s){
        //初始化marked数组
        this.marked=new boolean[G.V()];
        //初始化跟顶点s相通的顶点数量
        this.count=0;
        dfs(G,s);
    }
    //使用深度优先搜索找出图中v顶点的所有相通顶点
    private void dfs(Graph G,int v){
        //把v顶点标识为已搜索
        marked[v]=true;

        //遍历顶点v的邻接表队列
        for (Integer w  : G.adj(v)) {
            //判断当前w顶点有没有被搜索过,没有就递归
            if (!marked[w]){
                dfs(G,w);
                //相通顶点数量+1
                count++;
            }
        }

    }
    //判断w顶点与s顶点是否相通
    public boolean marked(int w){
        return marked[w];
    }
    //获取与顶点s相通的所有顶点的总数
    public int count(){
        return count;
    }

}
public class DepthSearchByTest {
    public static void main(String[] args) {
        //准备Graph对象
        Graph G = new Graph(13);
        G.addEdge(0,5);
        G.addEdge(0,1);
        G.addEdge(0,2);
        G.addEdge(0,6);
        G.addEdge(5,3);
        G.addEdge(5,4);
        G.addEdge(3,4);
        G.addEdge(6,4);
        G.addEdge(7,8);
        G.addEdge(9,11);
        G.addEdge(9,10);
        G.addEdge(9,12);
        G.addEdge(11,12);
        //准备深度优先搜索对象
        DepthFirstSearch search = new DepthFirstSearch(G, 0);
        //测试与某个顶点想通的顶点数量
        int count =search.count();
        System.out.println("与起点0想通的顶点数量为"+count);
        //测试某个顶点与起点是否相通
        boolean marked = search.marked(5);
        System.out.println("顶点5与起点0是否想通"+marked);
        boolean marked1 = search.marked(7);
        System.out.println("顶点7与起点0是否想通"+marked1);
    }

}
与起点0想通的顶点数量为6
顶点5与起点0是否想通true
顶点7与起点0是否想通false

广度优先搜索 

API设计 

类名BreadthFirstSearch
构造方法BreadthFirstSearch(Graph G,int s):构造广度优先搜索对象,使用广度优先搜索找出图中s顶点的所有相邻顶点
成员方法

1.private void bfs(Graph G,int v):使用广度优先搜索找出G图中v顶点的所有相邻顶点

2.public boolean marked(int w):判断w顶点与s顶点是否相通

3.public int count():获取与顶点s相通的所有顶点的总数

成员变量

1.private boolean[] marked:索引代表顶点,值表示当前顶点是否已经被搜索

2.private int count :记录有多少个顶点与s顶点相通

3.private Queue<Integer> waitSearch: 用来存储待搜索邻接表的点

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

    //构造深度优先搜索对象,使用深度优先搜索找出s顶点的所有相邻顶点
    public BreadthFirstSearch(Graph G, int s) {
        //初始化marked数组
        this.marked = new boolean[G.V()];
        //初始化跟顶点s相通的顶点数量
        this.count = 0;
        //初始化队列
        this.waitSearch = new LinkedList<>();
        bfs(G, s);
    }

    //使用广度优先搜索找出图中v顶点的所有相通顶点
    private void bfs(Graph G, int v) {
        //把v顶点标识为已搜索
        marked[v] = true;
        //让顶点v进入队列,待搜索
        waitSearch.offer(v);
        System.out.print("节点" +v+"广度遍历顺序为:" + v);
        //通过循环,如果队列不为空,则从队列中弹出一个待搜索的顶点进行搜索
        while (!waitSearch.isEmpty()) {
            //弹出一个待搜索的顶点
            Integer wait = waitSearch.poll();
            //遍历wait顶点的邻接表
            for (Integer w : G.adj(wait)) {
                //该顶点还没被搜索过 对其进行搜索
                if (!marked(w)) {
                    marked[w] = true;
                    //将节点放入队列中,用于后续的获取该节点的子节点
                    waitSearch.offer(w);
                    //让相通的顶点+1;
                    count++;
                    System.out.print(" " + w);
                }
            }
        }
        System.out.println();
    }

    //判断w顶点与s顶点是否相通
    public boolean marked(int w) {
        return marked[w];
    }

    //获取与顶点s相通的所有顶点的总数
    public int count() {
        return count;
    }

}
public class BreadSearchByTest {
    public static void main(String[] args) {
        //准备Graph对象
        Graph G = new Graph(13);
        G.addEdge(0,5);
        G.addEdge(0,1);
        G.addEdge(0,2);
        G.addEdge(0,6);
        G.addEdge(5,3);
        G.addEdge(5,4);
        G.addEdge(3,4);
        G.addEdge(6,4);
        G.addEdge(7,8);
        G.addEdge(9,11);
        G.addEdge(9,10);
        G.addEdge(9,12);
        G.addEdge(11,12);
        //准备深度优先搜索对象
        BreadthFirstSearch search = new BreadthFirstSearch(G, 0);
        //测试与某个顶点想通的顶点数量
        int count =search.count();
        System.out.println("与起点0相通的顶点数量为"+count);
        //测试某个顶点与起点是否相通
        boolean marked = search.marked(5);
        System.out.println("顶点5与起点0是否相通"+marked);
        boolean marked1 = search.marked(7);
        System.out.println("顶点7与起点0是否相通"+marked1);
    }

}
节点0广度遍历顺序为:0 5 1 2 6 3 4
与起点0相通的顶点数量为6
顶点5与起点0是否相通true
顶点7与起点0是否相通false

图的应用

public class TrafficProjectTest {
    public static void main(String[] args) {
        //准备Graph对象
        Graph G = new Graph(20);
        G.addEdge(0,1);
        G.addEdge(6,9);
        G.addEdge(3,8);
        G.addEdge(5,11);
        G.addEdge(2,12);
        G.addEdge(6,10);
        G.addEdge(4,8);
        DepthFirstSearch search1 =new DepthFirstSearch(G,9);
        BreadthFirstSearch search2 = new BreadthFirstSearch(G,9);
        System.out.println( "顶点10是否和顶点9相通 "+search1.marked(10));
        System.out.println("顶点8是否和顶点9相通 "+search2.marked(8));

    }
}



console:
节点9广度遍历顺序为:9 6 10
顶点10是否和顶点9相通 true
顶点10是否和顶点9相通 false

路径查找

 API设计

类名DepthFirstPaths
构造方法DepthFirstPaths(Graph G,int s): 构造深度优先搜索对象,使用深度优先搜索找出G图中起点为s的所有路径
成员方法

1.private void dfs(Graph G,int v):使用深度优先搜索找出图中v顶点的所有相邻顶点

2.public boolean hasPathTo(int v):判断v顶点与s顶点是否存在路径

3.public Stack<Integer> pathTo(int v):找出从起点s到顶点v的路径(就是该路径经过的顶点)

成员变量

1.private boolean[] marked:索引代表顶点,值表示当前顶点是否已经被搜索

2.private int s: 起点

3.private int[] edgeTo: 索引代表顶点,值代表从起点s到当前顶点路径上的最后一个顶点

public class DepthFirstPaths {
    //索引代表顶点,值代表当前顶点是否已被搜索
    private boolean[] marked;
    //起点
    private int s;
    //索引代表顶点,值代表从起点到当前顶点路径上的最后一个顶点
    private int[] edgeTo;
    //构造深度优先搜索对象,使用深度优先搜索找出G图中起点为s的所有路径

    public DepthFirstPaths(Graph G, int s) {
        //初始化marked数组
        this.marked = new boolean[G.V()];
        //初始化起点
        this.s = s;
        //初始化edgeTo数组
        this.edgeTo = new int[G.V()];
        dfs(G,s);
    }
    //使用深度优先搜索找出G图中v顶点的所有相邻顶点
    private void dfs(Graph G,int v){
        //把v表示为已搜索
        marked[v]=true;
        //遍历v的邻接表,拿到每个相邻的顶点,继续递归搜索
        for (Integer w: G.adj(v)){
            //如果顶点w没有被搜索,则继续递归搜索
            if(!marked[w]){
                edgeTo[w]=v;//到达w路径上最后一个顶点是v
                dfs(G,w);
            }
        }
    }
    //判断w顶点与s顶点是否存在路径
    public boolean hasPathTo(int v){



        return marked[v];
    }
    //找出从起点s到顶点v的路径(就是该路程经过的顶点)
    public Stack<Integer> pathTo(int v){
        if (!hasPathTo(v)){
            return null;
        }
       //创建栈对象
        Stack<Integer> path = new Stack<>();
        //通过循环,从顶点v开始,一直往前找,到找到起点为止
        for (int x = v; x != s; x=edgeTo[x]) {
            path.push(x);
            
        }
        //把起点s放到栈中
        path.push(s);
        return path;
    }
}
public class DepthFirstPathsTest {
    public static void main(String[] args) throws IOException {
        //构建缓冲读取流BufferReader
        BufferedReader br = new BufferedReader(new InputStreamReader
                (DepthFirstPathsTest.class.getResourceAsStream("road_find.txt")));
        //读取第一行数据
        int total = Integer.parseInt(br.readLine());
        //根据第一行数据构建一副图Graph
        Graph G = new Graph(total);
        //读取第二行数据
        int edgeNumbers = Integer.parseInt(br.readLine());
        //继续通过循环读取每一条边关联的两个顶点,调用addEdge方法添加边
        for (int i = 1; i < edgeNumbers; i++) {
            String edge = br.readLine();//0 1
            String[] str = edge.split(" ");
            int v = Integer.parseInt(str[0]);
            int w = Integer.parseInt(str[1]);
            G.addEdge(v,w);

        }
        //构建路径查找对象,并设置起点为0
        DepthFirstPaths paths = new DepthFirstPaths(G,0);
        //调用pathTo(4),找到起点0到终点4的路径,返回stack
        Stack<Integer> path = paths.pathTo(4);
        StringBuilder sb = new StringBuilder();
        //遍历对象
        for (Integer v : path) {
            sb.append(v+"-");
        }
        sb.deleteCharAt(sb.length()-1);
        System.out.println(sb);
    }
}
4-3-2-0

有向图

相关术语

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

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

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

有向环:

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

有向图API设计

类名Digraph
构造方法Digraph(int V): 创建一个包含v个顶点但不包含边的有向图
成员方法

1.public int V(): 获取图中顶点的数量

2.public int E(): 获取图中边的数量

3.public void addEdge(int v,int w):向有向图中添加一条边v->w

4.public Queue<Integer> adj(int v):获取由v指出的边所连接的所有顶点

5.private Digraph reverse(): 该图的反向图 

成员变量

1.private final int V:记录顶点的数量

2.private int E: 记录边的数量

3.private Queue<Integer>[] adj: 邻接表

        在API中设计了一个反向图,其因为有向图的实现中,用adj方法获取出来的是由当前顶点v指向的其他顶点,如果能得到其反向图,就可以很容易得到指向v的其他顶点。

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

    public Digraph( int v) {
        //初始化顶点数
        this.V = v;
        //初始化边的数量
        E = 0;
        //初始化邻接表
        this.adj = new Queue[v];
        for (int i = 0; i < adj.length; i++) {
            adj[i]= new LinkedList<>();

        }
    }
    //获取顶点数目
    public int V(){
        return V;
    }
    //获取边的数目
    public int E(){
        return E;
    }
    //向图中添加一条边  v->w
    public void addEdge(int v,int w){
        //只需要让顶点w出现在顶点v的邻接表中,因为边是有方向的,
        // 最终顶点v的邻接表中存储的相邻顶点的含义是:v->其他顶点
        adj[v].offer(w);
        E++;
    }
    //获取v指出的边连接的所有顶点
    public Queue<Integer> adj(int v){
        return adj[v];
    }
    //该图的反向图
    private Digraph reverse(){
        //创建有向图对象
        Digraph r = new Digraph(V);
        //遍历所有顶点
        for (int v = 0; v < V; v++) {
            //获取由该顶点指出的所有边
            for (Integer w : adj[v]) {//原图是v指向w的边
             r.addEdge(w,v);//w->v
            }
        }
        return r;
    }
}

拓扑排序

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

检测有向图中的环

        如果一副有向图中存在环,那么就无法对其拓扑排序了,要使用拓扑排序解决优先级问题,首先得保证图中没有环的存在。

检测有向环的API设计 

类名DirectedCycle
构造方法DirectedCycle(Digraph G): 创建一个检测环对象,检测图中是否有环
成员方法

1.private void dfs(Digraph G ,int v): 基于深度优先搜索,检测图中是否有环

2.public boolean hasCycle(): 判断图中是否有环

成员变量

1.private boolean[] marked: 索引代表顶点,指标是当前顶点是否已经被搜索

2.private boolean hasCycle: 记录图中是否有环

3.private boolean[] Onstack: 索引代表顶点,使用栈的思想,记录当前顶点有没有已经处在正在搜索的有向路径上

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

    //创建一个检测环对象,检测图G中是否有环
    public DirectedCycle(Digraph G) {
        //初始化marked数组
        this.marked = new boolean[G.V()];
        //初始化hasCycle
        this.hasCycle = false;
        //初始化onStack数组
        this.onstack = new boolean[G.V()];
        //找到图中每一个顶点,让每一个顶点作为入口,调用dfs进行搜索
        for (int v = 0; v < G.V(); v++) {
            //判断如果当前顶点还没被搜索过,调用dfs进行搜索
            if(! marked[v]){
                dfs(G,v);
            }
        }
    }

    //基于深度优先搜索,检测图中是否有环
    private void dfs(Digraph G, int v) {
        //把顶点v表示为已搜索
        marked[v] = true;
        //把当前顶点进栈,true
        onStack[v]=true;
        //进行深度搜索
        for (Integer w : G.adj(v)) {
            //如果当前顶点w没被搜索过,则继续递归调用dfs方法完成深度优先搜索
            if(!marked[w]){
                dfs(G,w);
            }
            //判断当前顶点w是否已经在栈中,证明当前顶点之前出于正在搜索的状态,
            // 那么现在又要搜索一次证明检测到环了
            if(onStack[w]){
                hasCycle=true;
                return;
            }
        }
        //把当前顶点出栈
        onStack[v]=false;
    }

    //判断图中是否有环
    public boolean hasCycle() {
        return hasCycle;
    }
}

基于深度优先的顶点排序

        如果要把图中的顶点生成线性序列其实是一件非常简单的事,深度优先搜索有一个特点,在连通子图上,每个顶点只会被搜索一次,如果在此基础上,添加一行代码·,只需将搜索的顶点放入到线性序列的数据结构中。

顶点排序的API设计

       

类名DepthFirstOrder
构造方法

DepthFirstPrder(Digraph G): 创建一个顶点排序对象,生成顶点线性序列;

成员方法

1.private void dfs(Digraph G,int v);  基于深度优先搜索,生成顶点线性序列

2.public Stack<Integer> reversePost(); 获取顶点线性序列

成员变量

1.private boolean[] marked; 索引代表顶点,值代表当前顶点是否已经被搜索

2.private Stack<Integer> 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<>();
        //找到图中每一个顶点,让每一个顶点作为入口,调用dfs进行搜索
        for (int v = 0; v < G.V(); v++) {
            //判断如果当前顶点还没被搜索过,调用dfs进行搜索
            if(! marked[v]){
                dfs(G,v);
            }
        }
    }
    //基于深度优先搜索,生成顶点线性序列
     private void dfs(Digraph G,int v){
         //把v顶点标识为已搜索
         marked[v]=true;

         //遍历顶点v的邻接表队列
         for (Integer w  : G.adj(v)) {
             //判断当前w顶点有没有被搜索过,没有就递归
             if (!marked[w]){
                 dfs(G,w);
             }
         }
         //让顶点进栈
         reversePost.push(v);
     }
     //获取顶点线性序列
    public  Stack<Integer> getReversePost(){
        return reversePost;
    }

}

拓扑排序实现

        已经实现了环的检测和顶点排序,基于一幅图,先检测有无环,无环顶点排序

API设计

类名ToPoLogical
构造方法

TopoLogical(Digraph G); 构造拓扑排序对象

成员方法

1.public boolean isCycle(); 判断图G是否有环

2.public Stack<Integer> order(); 获取拓扑排序的所有顶点

成员变量1.private Stack<Integer> order; 顶点的拓扑排序
//拓扑排序
public class TopoLogical {
    //顶点的拓扑排序
    private Stack<Integer> order;

    //构造拓扑排序对象
    public TopoLogical(Digraph G) {
        //创建一个检测环的对象
        DirectedCycle cycle = new DirectedCycle(G);
        //判断G图中有没有环,如果没有环,则进行顶点排序
        if(!cycle.hasCycle()){
            DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G);
            order= depthFirstOrder.getReversePost();
        }

    }

    //判断图G是否有环
    public boolean isCycle() {
        return order==null;
    }

    //获取拓扑排序的所有顶点
    public Stack<Integer> order() {
            return order;
    }
}

拓扑排序测试

        

public class TopoLogicalTest {

    public static void main(String[] args) {
        Digraph digraph = new Digraph(6);
        digraph.addEdge(0,2);
        digraph.addEdge(0,3);
        digraph.addEdge(2,4);
        digraph.addEdge(3,4);
        digraph.addEdge(4,5);
        digraph.addEdge(1,3);

        //通过TopoLogical对有向图的顶点进行排序
        TopoLogical topoLogical = new TopoLogical(digraph);
        Stack<Integer> order =topoLogical.order();
        StringBuilder sb = new StringBuilder();
        System.out.println(order.peek());
        //只能弹栈方式遍历路径
       while(!order.isEmpty()){
           sb.append(order.pop()+"->");
        }
        String str = sb.toString();
        int index = str.lastIndexOf("->");
        str=str.substring(0,index);
        System.out.println(str);
    }
}
1
1->0->3->2->4->5

加权无向图

        加权无相图是一种为每条边相关联一个权重值或是成本的图的模型。这种能够自然地表示许多应用。例如从西安飞纽约,权重就可表示时间成本或金钱成本。

加权无相图的表示

   加权无向图的边就不能简单的使用v-w两个顶点表示了,而必须要给边关联一个权重值,因此我们可以用对象来描述一条边。

API设计

类名Edge implements Comparable<Edge>
构造方法Edge(int v,int w,double weight); 通过顶点v和w,以及权重weight值构造一个对象
成员方法

1.public double weight(); 获取边的权重

2.public int either(); 获取边上的一个点

3.public int other( int vertex); 获取边上除了顶点vertex外的另外一个顶点

4.public int compareTo(Edge that); 比较当前边和参数that边的权重,如果当前边权重大,返回1,如果一样大,返回0,如果当前权重小,返回-1

成员变量

1.private final int v; 顶点一

2.private final int w; 顶点二

3.private final double weight; 当前边的权重

public class Edge implements Comparable<Edge>{
    //顶点一
    private final int v;
    //顶点二
    private final int w;
    // 当前边的权重
    private final double weight;

    public Edge(int v, int w, double weight) {
        this.v = v;
        this.w = w;
        this.weight = weight;
    }
    //获取边的权重
    public double weight(){
        return weight;
    }
    //获取边上的一个点
    public int either(){
        return v;
    }
    //获取边上除了顶点vertex外的另外一个顶点
    public int other( int vertex){
        if (vertex==v){
            return w;
        }else {
            return v;
        }

    }

    //比较当前边和参数o边的权重,如果当前边权重大,返回1,
    // 如果一样大,返回0,如果当前权重小,返回-1
    @Override
    public int compareTo(Edge o) {
        //使用一个遍历记录比较的结果
        int tmp;
        //如果当前边的权重值大,tmp=1;
        if (this.weight>o.weight){
            tmp=1;
        }
        //如果当前边的权重值小,tmp=-1;
        else if (this.weight<o.weight){
            tmp=-1;
        }else {
            tmp=0;
        }
        //如果当前边的权重值与o相同,tmp=0;
        return tmp;
    }
}

 加权无向图的实现

API设计

类名EdgeWeightedGraph
构造方法EdgeWeightedGraph(int V); 创建一个含有v各顶点的空加权无向图
成员方法

1.public int V(); 获取图中顶点的数量

2.public int E(); 获取图中边的数量

3.public void addEdge(Edge e);向加权无向图中添加一条边e

4.public Queue<Edge> adj(int v); 获取和顶点v相关联的所有边

5.public Queue<Edge> edges(); 获取加权无向图的所有边

成员变量

1.private final int V; 记录顶点数量

2.private int E; 记录边的数量

3.private Queue<Edge>[] adj; 邻接表

public class EdgeWeightedGraph {
    //记录顶点数量
    private final int V;
    //记录边的数量
    private int E;
    //邻接表
    private Queue<Edge>[] adj;
    //创建一个含有v各顶点的空加权无向图
    public EdgeWeightedGraph(int v) {
        this.V = v;
        E = 0;
        this.adj = new LinkedList[v];

        for (int i = 0; i < adj.length; i++) {
          adj[i]=new LinkedList<>();
            
        }
    }
    //获取图中顶点的数量
    public int V(){
        return V;
    }
    //获取图中边的数量
    public int E(){
        return E;
    }
    //向加权无向图中添加一条边e
    public void addEdge(Edge e) {
        //需要让边e同时出现在e这个边的两个顶点的邻接表中
        int v=e.either();
        int w = e.other(v);
        //边存入队列中
        adj [v].offer(e);
        adj [w].offer(e);
        //边的数量+1
        E++;
    }
    // 获取和顶点v相关联的所有边
    public Queue<Edge> adj(int v) {
        return adj[v];
    }
    //获取加权无向图的所有边
    public Queue<Edge> edges() {
        //创建一个队列对象,存储所有边
        Queue<Edge> allEdges = new LinkedList<>();
        //遍历图中每一个顶点,找到该顶点的邻接表,邻接表中存储了该顶点关联的每一条边
        //因为这是无向图同一条边出现在了它关联两个顶点的邻接表中,需要让一条边只记录一次
        for (int v = 0; v <V; v++) {
           //遍历v顶点的邻接表,找到每一条和v关联的边
            for(Edge e:adj(v)){
               if ( e.other(v)<v){
                    allEdges.offer(e);
               }
            }

        }
        return allEdges;
    }
}

最小生成树

定义:

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

 约定:

        只考虑连通图。最小生成树的定义说明它只能存在于连通图中,如果图不是连通的,那么分别计算每一个连通图子图的最小生成树,合并到一起称为最小生成森林。

        所有边的权重都各不相同。如果不同的边权重可以不同,那么一副图的最小生成树就可能不唯一了,虽然我们的算法可以处理这种情况,但为了好理解,我们约定所有边的权重都各不相同。 

最小生成树原理

树的性质:

切分定理

要从一副连通图中找到该图的最小生成树,需要通过切分定理完成。

切分:将图中的所有顶点按照某些规则分为两个非空且没有交集的集合。

横切边:连接两个属于不同集合的顶点的边称之为横切边。

例如我们将图中的顶点切分为两个集合,灰色顶点属于一个集合,白色顶点属于另外一个集合,那么效果如下·:

切分定理:

        在一副加权图中,给定任意的切分,它的横切边中的权重值最小者必然属于图中的最小生成树。 

        

注意:一次切分产生的多个横切边中,权重最小的边不一定是所有横切边中唯一属于图中的最小生成树的边。

贪心算法

        贪心算法是计算最小生成树的基础算法,它的基本原理就是切分定理,使用切分定理找到最小生成树的一条边,不断的重复直到找到最小生成树的所有边。如果有v个顶点,那么热需要找到v-1条边,就可仪表室该图的最小生成树。

 

Prim算法(领接表)

Prim算法是计算最小生成数的算法,它的每一步都会为一棵生成中的树添加一条边,一开始这棵树只有一个顶点,然后会向它添加v-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入到树中。

prim算法的切分规则:

        把最小生成树中顶点看做是一个集合,把不在最小生成树的顶点看做是另外一个集合。

Prim算法API设计 

类名PrimMST
构造方法

PrimMST(EdgeWeightedGraph G); 根据一副加权无向图,创建最小生成树计算对象;

成员方法

1.private void visit(EdgeWeightedGraph G,int v); 将顶点v添加到最小生成树中,并且更新数据

2.public Queue<Edge> edges(); 获取最小生成树的所有边

成员变量

1.private Edge[] edgeTo; 索引代表顶点,值代表当前顶点和最小生成树之间的最短边

2.private double[] distTo; 索引代表顶点,值代表当前顶点和最小生成树之间的最短边的权重

3.private boolean[] marked; 索引代表顶点,如果当前顶点已经在树中,则值为true,否则为false

4.private indexMinPriorityQueue<Double>pq;存放树中顶点与非树中顶点之间的有效横切边

 

public class PrimMST {
    //索引代表顶点,值表示当前顶点和最小生成树之间的最短边
    private Edge[] edgeTo;
    //索引代表顶点,值表示当前顶点和最小生成树之间的最短边的权重
    private double[] disTo;
    //索引代表顶点,如果当前顶点已经在树中,则值为true,否则为false
    private boolean[] marked;
    //存放树中顶点与非树中顶点之间的有效横切边
    private IndexMinPriorityQueue<Double> pq;

    public PrimMST(EdgeWeightedGraph G) {
        //初始化edgeTo
        this.edgeTo = new Edge[G.V()];
        //初始化distTo
        this.disTo = new double[G.V()];
        for (int i = 0; i < disTo.length; i++) {
            disTo[i] = Double.POSITIVE_INFINITY;
        }
        //初始化marked
        this.marked = new boolean[G.V()];
        //初始化pq
        pq = new IndexMinPriorityQueue<Double>(G.V());
        //默认顶点0进入到树中,但是树中只有一个顶点0,
        // 因此顶点默认没有和其他的顶点相连,所以disTo对应位置处的值存储0.0
        disTo[0] = 0.0;
        pq.insert(0, 0.0);

        //遍历索引最小优先队列,拿到最小和N切边对应的顶点,把该顶点加入到最小生成树中
        while (!pq.isEmpty()) {

             visit(G, pq.delMin());

        }
    }

    //将顶点v添加到最小生成树中,并且更新数据
    private void visit(EdgeWeightedGraph G, int v) {
         //把顶点v添加到最小生成树中
        marked[v]=true;
        //更新数据
        for (Edge e: G.adj(v)){
            //获取e边的另外一个顶点(当前顶点是v)
            int w = e.other(v);
            //判断另外一个顶点是不是已经在树中,如果在树中,则不做任何处理
            // ,如果不在树中,更新数据
            if (marked[w]){
                continue;
            }
            //判断边e的权重是否小于w顶点到树中已经存在的最小边的权重
            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> allEdges = new LinkedList<>();
        //遍历edgeTo数据,索引每一条边,如果不为null,则添加到队列中
        for (int i=0;i<edgeTo.length;i++){
            if (edgeTo[i]!=null){
                allEdges.offer(edgeTo[i]);
            }
        }
        return  allEdges;
    }

}
public class PrimMSTTest {
    public static void main(String[] args) throws IOException {
        //准备一副加权无向图
        BufferedReader br = new BufferedReader(new InputStreamReader(PrimMSTTest.class.getResourceAsStream("min_creat_tree_test.txt")));

        int total = Integer.parseInt(br.readLine());
        EdgeWeightedGraph G = new EdgeWeightedGraph(total);
        int edgeNumbers = Integer.parseInt(br.readLine());
            for (int e =1;e<edgeNumbers;e++){
                String line = br.readLine();//4 5 0.35
                String[] str = line.split(" ");
                int v = Integer.parseInt(str[0]);

                int w = Integer.parseInt(str[1]);
                double weight = Double.parseDouble(str[2]);

                //构建加权无向边
                Edge edge = new Edge(v,w,weight);

                G.addEdge(edge);

            }
        //创建一个primMST对象 ,计算加权无向图中的最小生成树
        PrimMST mst = new  PrimMST(G);
        //获取最小生成树中的所有边

        Queue<Edge> edges =mst.edges();

        //遍历打印所有的边
        for (Edge e : edges) {
            int v = e.either();
            int w = e.other(v);
            double weight = e.weight();
            System.out.println(v+"-"+w+"-"+weight);
        }
    }
}

 Prim算法(领接矩阵)

 

public class PrimAlgorithm {

    public static void main(String[] args) {
        //测试图
        char[] data = new char[]{'A','B','C','D','E','F','G'};
        int verxs = data.length;
        //邻接矩阵的关系使用二维数组表示
        int [][]weight= new int[][]{
                {1000,5,7,1000,1000,1000,2},{5,1000,1000,9,1000,1000,3}
                ,{7,1000,1000,1000,8,1000,1000},{1000,9,1000,1000,1000,4,1000}
                ,{1000,1000,8,1000,1000,5,4},{1000,1000,1000,4,5,1000,6},
                {2,3,1000,1000,4,6,1000}
        };
        //创建MyGraph对象
        MyGraph graph = new MyGraph(verxs);
        //创建一个MinTree对象
        MinTree minTree = new MinTree();
        minTree.creatGraph(graph,verxs,data,weight);
        //输出
        minTree.showGraph(graph);
        //测试prim
        minTree.prim(graph,0);

    }


}
//创建最小生成树
class MinTree {
    //创建图的邻接矩阵
    public void creatGraph(MyGraph graph,int verxs,char data[] ,int[][] weight ){
        int i,j;
        for (i = 0; i < verxs; i++) {
            graph.data[i]= data[i];
            for (j= 0; j < verxs; j++) {
               graph.weight[i][j]= weight[i][j];

            }
            
        }
    }
    //显示图的邻接矩阵
    public void showGraph(MyGraph graph){
        for (int[] link : graph.weight) {
            System.out.println(Arrays.toString(link));
        }
    }

    public void prim(MyGraph graph,int v){
        //visited[] 标记节点(顶点)是否被访问过
        int visited[] = new int[graph.verx];
        //visited[] 默认元素的值都是0,表示没被访问过
        for (int i = 0; i < visited.length; i++) {
             visited[i]=0;
        }
        //把当前这个节点标记为已访问
        visited[v]=1;
        //h1和h2记录两个顶点的下标
        int h1 = -1;
        int h2 = -1;
        int minWeight = 1000;//将minWeight 初始成一个大数,后面在遍历过程中,会被替换
        for (int k=1;k<graph.verx;k++){//因为有graph.verx顶点,prim算法结束后,有graph.verx-1边

            //确定每一次生成的子图,和哪个节点的距离最近
            for (int i = 0; i < graph.verx; i++) {//i节点表示被访问过的节点
                for (int j = 0; j < graph.verx; j++) {//j节点表示还没被访问过的节点
                   if (visited[i]==1&&visited[j]==0&&graph.weight[i][j]<minWeight){
                       //替换minWeight
                       minWeight = graph.weight[i][j];
                       h1=i;
                       h2=j;

                   }

                }

            }
            //找到一条边是最小
            System.out.println("边<"+graph.data[h1]+","+graph.data[h2]+"> 权值:"+minWeight);
            //将当前这个节点标记为已经访问
            visited[h2]=1;
            //minWeight 重新设置为最大值 1000
            minWeight=1000;
        }

    }
}

class MyGraph{
    int verx;//表示图的节点个数
    char[] data;//存放节点数据
    int[][] weight;//存放边,领接矩阵

    public MyGraph(int verx) {
        this.verx = verx;
        this.data = new char[verx];
        this.weight = new int[verx][verx];
    }
}
[1000, 5, 7, 1000, 1000, 1000, 2]
[5, 1000, 1000, 9, 1000, 1000, 3]
[7, 1000, 1000, 1000, 8, 1000, 1000]
[1000, 9, 1000, 1000, 1000, 4, 1000]
[1000, 1000, 8, 1000, 1000, 5, 4]
[1000, 1000, 1000, 4, 5, 1000, 6]
[2, 3, 1000, 1000, 4, 6, 1000]
边<A,G> 权值:2
边<G,B> 权值:3
边<G,E> 权值:4
边<E,F> 权值:5
边<F,D> 权值:4
边<A,C> 权值:7

 krusskai算法

 API设计

类名KruskalMST
构造方法 public KruskalMST(EdgeWeightGraph G );根据一副加权无向图,创建最小生成树计算对象
成员方法1.public Queue<Edge> edges()获取最小生成树的所有边
成员变量

1.private Queue<Edge> mst; 保存最小生成树的所有边

2.private UF_Tree_Weight uf; 索引代表顶点,使用uf.connect(v,w)可以判断顶点v和顶点w是否在同一棵树中,使用uf.union(v,w)就可以把顶点v所在的树和顶点w所在的树合并

3.private MinPriorityQueue<Edge> pq; 存储图中所有的边,使用最小优先队列对边按照权重进行排序

krusal算法原理

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

  

public class KruskalMST {

    public static void main(String[] args) throws IOException {

        //准备一副加权无向图
        BufferedReader br = new BufferedReader(new InputStreamReader(PrimMSTTest.class.getResourceAsStream("min_creat_tree_test.txt")));

        int total = Integer.parseInt(br.readLine());
        EdgeWeightedGraph G = new EdgeWeightedGraph(total);
        int edgeNumbers = Integer.parseInt(br.readLine());
        for (int e =1;e<edgeNumbers;e++){
            String line = br.readLine();//4 5 0.35
            String[] str = line.split(" ");
            int v = Integer.parseInt(str[0]);

            int w = Integer.parseInt(str[1]);
            double weight = Double.parseDouble(str[2]);

            //构建加权无向边
            Edge edge = new Edge(v,w,weight);

            G.addEdge(edge);

        }
        KruskalMST mst =new KruskalMST(G);
        //获取最小生成树中的所有边

        Queue<Edge> edges =mst.edges();

        //遍历打印所有的边
        for (Edge e : edges) {
            int v = e.either();
            int w = e.other(v);
            double weight = e.weight();
            System.out.println(v+"-"+w+"-"+weight);
        }

    }
    private Queue<Edge> mst; //保存最小生成树的所有边

private UF_Tree_Weight uf; //索引代表顶点,使用uf.connect(v,w)可以判断顶点v和顶点w是否在同一棵树中,使用uf.union(v,w)就可以把顶点v所在的树和顶点w所在的树合并

private MinPriorityQueue<Edge> pq; //存储图中所有的边,使用最小优先队列对边按照权重进行排序

    public KruskalMST(EdgeWeightedGraph G) {
        //初始化mst
        this.mst = new LinkedList<>();
        //初始化uf
        this.uf = new UF_Tree_Weight(G.V());
        //初始化pq
        this.pq = new MinPriorityQueue<>(G.E()+1);
        //把图中所有的边存储到pq中
        for (Edge e : G.edges()) {
            pq.insert(e);
        }

        //遍历(pq)队列,拿到最小权重的边,进行处理
        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);

            //让边e进入到队列中
            mst.offer(e);

        }
    }

    //获取最小生成树的所有边
    public Queue<Edge> edges(){
        return mst;
    }
}

console:
0-7-0.16
2-3-0.17
1-7-0.19
0-2-0.26
5-7-0.28
4-5-0.35
6-2-0.4

加权有向图

边的表示

API设计

类名DirectedEdge
构造方法DirectedEdge(int v,double weight); 通过顶点v和w,以及权重weight值构造一个边对象
成员方法

1.public double weight(); 获取边的权重值

2.public int from(); 获取有向边的起点

3.public int to(); 获取有向边的终点

成员变量

1.private final int V;起点

2.private final int W ; 终点

3.private final double weight ;当前边的权值

加权有向图的实现

        在有向图的基础上,把边的表示切换为DirectedEdge对象即可。

API设计

类名EdgeWeightedDigraph
构造方法EdgeWeightedDiraph(int V); 创建一个含有v各顶点的空加权有向图
成员方法

1.public int V(); 获取图中顶点的数量

2.public int E(); 获取图中边的数量

3.public void addEdge(DirectedEdge e);向加权有向图中添加一条边e

4.public Queue<DirectedEdge> adj(int v); 获取和顶点v相关联的所有边

5.public Queue<DirectedEdge> edges(); 获取加权有向图的所有边

成员变量

1.private final int V; 记录顶点数量

2.private int E; 记录边的数量

3.private Queue<DirectedEdge>[] adj; 邻接表

public class EdgeWeightedDigraph {
    //记录顶点数量
    private final int V;
    //记录边的数量
    private int E;
    //邻接表
    private Queue<DirectedEdge>[] adj;
    //创建一个含有v各顶点的空加权无向图
    public EdgeWeightedDigraph(int v) {
        this.V = v;
        E = 0;
        this.adj = new LinkedList[v];

        for (int i = 0; i < adj.length; i++) {
          adj[i]=new LinkedList<>();
            
        }
    }
    //获取图中顶点的数量
    public int V(){
        return V;
    }
    //获取图中边的数量
    public int E(){
        return E;
    }
    //向加权有向图中添加一条边e
    public void addEdge(DirectedEdge e) {
        //需要让边e出现在起点的邻接表中
        int v=e.from();

        //边存入队列中
        adj [v].offer(e);

        //边的数量+1
        E++;
    }
    // 获取和顶点v相关联的所有边
    public Queue<DirectedEdge> adj(int v) {
        return adj[v];
    }
    //获取加权有向图的所有边
    public Queue<DirectedEdge> edges() {
        //创建一个队列对象,存储所有边
        Queue<DirectedEdge> allEdges = new LinkedList<>();
        //遍历图中的每一个顶点,得到该顶点的邻接表,遍历得到每一条边,添加到队列中返回即可
        for (int v = 0; v <V; v++) {
           //遍历v顶点的邻接表,找到每一条和v关联的边
            for(DirectedEdge e:adj(v)){
                    allEdges.offer(e);
            }

        }
        return allEdges;
    }
}

最短路径

定义及性质

定义:

        在一副加权有向图中,从顶点s到顶点t的最短路径是所有从顶点s到顶点t的路径中总权重最小的那条路径

性质:

1.路径具有方向性;

2.权重不一定等价于距离,权重可以是距离、时间、话费等内容、权重最小指的是成本最低

3.只考虑连通图,一副图中并不是所有的顶点都是可达的,如果s和t不可达,那么它们之间也就就不存在最短路径,为了简化问题,这里只考虑连通图

4.最短路径不一定是唯一的,从一个顶点到达另外一个顶点的权重最小的路径可能会有很多条,这里只需要找出一条即可

最短路径树:

给定一副加权有向图和一个顶点s,以s为起点的一棵最短路径树是图的一副子图,它包含顶点s可达的所有顶点。这棵有向树的根节点为s,树的每条路径都是有向图中的一条最短路径。 

最短路径树API设计

类名DijkstraSP
构造方法public DijkstraSP(EdgeWeightedDigraph G,int s); 根据一副加权有向图G和顶点s,创建一个计算顶点为s的最短路径树对象
成员方法

1.private void relax(EdgeWeightedDigraph G,int v); 松弛图G中的顶点v

2.public double distTo(int v); 获取从顶点s到顶点v的最短路径的总权重

3.public boolean hasPathTo(int v); 判断顶点s到顶点v是否可达

4.public Queue<DirectedEdge> pathTo(int v); 查询从起点s到顶点v的最短路径中所有的边

; 成员变量

1.private DirectedEdge[] edgeTo; 索引代表顶点,值表示从顶点s到当前顶点的最短路径上的最后一条边

2.private doouble[] disTo; 索引代表顶点,值从顶点s到当前顶点的最短路径的总权重

3.private IndexMinPriorityQueue<Double> pq; 存放树中顶点与非树中顶点之间的有效横切边

松弛技术

        松弛来源于生活:一条橡皮筋沿着两个·顶点的某条路径紧紧展开,如果这两个顶点之间的路径不只一条,还有存在更短的路径,那么把皮筋转移到更短的路径上,皮筋就可以松了

松弛原理恰好可以计算路径树。

在API中,需要用到两个成员变量edge和distTo,分别存储边和权重。一开始给定一幅图G和顶点,我们只知道图的边以及这些边的权重,其他的一无所知,此时初始化顶点s到顶点s的最短路径的总权重diso[s]=0;顶点s到其他顶点的总权重默认为无穷大,随着算法的执行,不断的使用松弛技术处理图的边和顶点,并按一定的条件更新edgeTo和distTo中的数据,最终就可以得到最短路径树。

边的松弛:

        放松边v->w意味着检查从s到w的最短路径是否先从s到v,然后再从v到w?

        如果是,则v-w这条边需要加入到最短路径树中,更新edgeTo和distTo中的内容:edgeTo[w]=表示v->w这条边的DirectedEdge对象,distTo[v]+v->w这条边的权重;

如果不是,则忽略v->w这条边。

顶点的松弛

顶点的松弛是基于边的松弛完成的,只需要把某个顶点指出的所有边松弛,那么该顶点就松弛完毕。例如要松弛顶点v,只需要遍历v的邻接表,把每一条边都松弛,那么顶点v就松弛了。

如果把起点设置为顶点0,那么找到起点0到顶点6的最短路径0-2-7-3-6的过程如下:

public class DijkstraSP {

    public static void main(String[] args) throws IOException {
        //创建一副加权有向图
        BufferedReader br = new BufferedReader
                (new InputStreamReader(DijkstraSP.class.getResourceAsStream("mainTest.txt")));

        int total = Integer.parseInt(br.readLine());
        EdgeWeightedDigraph G = new EdgeWeightedDigraph(total);
        int edgeNumbers = Integer.parseInt(br.readLine());
        for (int e =1;e<edgeNumbers;e++){
            String line = br.readLine();//4 5 0.35
            String[] str = line.split(" "); 
            int v = Integer.parseInt(str[0]);

            int w = Integer.parseInt(str[1]);
            double weight = Double.parseDouble(str[2]);

            //构建加权无向边
            DirectedEdge edge = new DirectedEdge(v,w,weight);

            G.addEdge(edge);

        }
        //创建DijkstraSP对象
        DijkstraSP dijkstraSP = new DijkstraSP(G,0);
        //查找最短路径 0-6
        Queue<DirectedEdge> edges = dijkstraSP.pathTo(6);
        //遍历打印
        for (DirectedEdge edge : edges) {
            System.out.println(edge.from()+"->"+edge.to()+"::"+edge.weight());
        }

    }

    private DirectedEdge[] edgeTo; //索引代表顶点,值表示从顶点s到当前顶点的最短路径上的最后一条边

    private double[] distTo; //索引代表顶点,值从顶点s到当前顶点的最短路径的总权重

    private IndexMinPriorityQueue<Double> pq; //存放树中顶点与非树中顶点之间的有效横切边

    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 e: G.adj(v)){
            //获取到该边的终点w
            int w =e.to();

            //通过松弛技术,判断从起点s到顶点w的最短路径是否需要光从顶点s到顶点v,然后由顶点v到顶点w
            if (distTo(v)+e.weight()<distTo(w)){
                distTo[w]=distTo[v]+e.weight();
                edgeTo[w]=e;

                //判断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 LinkedList<>();

        while (true){
            DirectedEdge e = edgeTo[v];
            if (e==null){
                break;
            }
            allEdges.offer(e);
            v=e.from();
        }
        return allEdges;
    }
}
3->6::0.52
7->3::0.39
2->7::0.34
0->2::0.26

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值