99-数据结构与算法(上篇)

news2024/11/27 21:05:05

数据结构与算法

数据结构和算法,一个非常古老的课题,工作的时候,一般只求程序能跑,并不太关注性能
一般情况下,我们尽量避坑,即避免这样:ArrayList Or LinkedList,哪个简单用哪个
实际上数据结构和算法是程序员的内功,架构搭的再好,技术使用的再新,如果没有好的数据结构设计和算法
系统也会出问题甚至崩塌,尤其是在互联网软件上,细节决定成败,练好内功尤为重要
数据结构与算法概述:
数据结构的概念:
什么是数据结构 :
数据结构(data structure)是计算机存储、组织数据的方式
数据结构是指相互之间存在一种或多种特定关系的数据元素的集合(百度百科)
或者说,就是将数据以某种方式进行保存,简单来说,就是存数据的,而且是在内存中存储数据(redis可以认为是这样,即redis可以认为是始终运行中的程序,将里面的数据进行使用,可以这样理解,来一个无限循环,其中每过10秒循环一次,且添加一个数组,通常定义唯一名称,比如UUID,那么添加的数组,可以将他认为就是我们存储的redis数据)
而不是内存的,那么通常以硬盘方式存储,比如mysql(通常以文件的形式)
常见的数据结构 :

在这里插入图片描述

上面只是某种结构的统称,实际上就算是直接的赋值,也能叫做数据存放的方式,即可以叫做数据结构
一般来说,我们对于数据的存放,一般也会影响到性能的,所以才会有对于的数组或者链表等等不同的数据存放方式
所以提高性能的不只是算法,数据结构也算
比如:
我们可以有大小顺序的存放某些数,自然比随机存放数可以做更多的事情,比如比较大小
我们可以自己定义一个有序大小的数组,虽然并没有自带,但是我们可以自己定义
而自带的数据结构中,有有序和无序,但只是对存放来说,但是有些输出可能是按照某些大小来排列的
比如hashmap的打印信息,在某些数据中
是排列的(但并不绝对,可以看如下博客:https://zhidao.baidu.com/question/1546390810197250187.html)
这都是为了使得数据更好的操作而已(在某个范围里)
算法的概念:
什么是算法:
算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令
算法代表着用系统的方法描述解决问题的策略机制
一句话描述:算法是一种解决特定问题的思路以及可以使得原来的问题更加的简便(如优化,这种就是区分算法的好坏)
比如:LRU(Least recently used:最近最少使用)算法
随机淘汰最近最少使用的数据,解决的就是当空间不够用时,应该淘汰谁的问题,这是一种策略,不是唯一的答案
因为我们也可以直接的随机淘汰一个(无论使用了多,还是使用了少),来解决空间不够用的问题,虽然实际上这并不友好(可能会出现某种问题,因为淘汰了使用的多的,可能使得导致用户流失),但是对于解决空间不够用来说,也可以操作
所以算法无对错,只有好和不好
即算法是:能够解决某些问题(如找出三个数的最大值,如1+1等于多少,这个也算是的,等等),也可以进行简便(找出三个数的最大值的方式有多种,我们可以找到更加简便的方式,或者使用更加简便的方式)
实际上算法在一定程度,需要数学的知识
比如在下游(也就是问题容易或者大致考虑性能的,性能:可以认为是程序运行得到正确结果所需要的内存和时间,该时间越低或者内存使用越少,一般性能越高,当然,如果时间低,但是内存使用超级多,那么性能也是低的,即需要总体考虑,一般他们是互相促进的,即内存使用的少,时间一般也会低,一般数据结构越好以及算法越好,通常性能也会更高,所以在程序中,我们也会以时间复杂度和空间复杂度来总体代表性能)的情况下,基本不需要数学,只需要合理的使用数据存放的方式以及某些循环操作
或者说,依靠数据结构以及循环(这里说明的该循环包括很多类型,而不只是对应的循环语句,这里要注意,如不只是for循环语句,这里只要是多次的操作都称为循环,比如for循环语句,数组扩容,递归等等,当然还有其他的,就不依次说明了,这里我们统一认为是循环,如果和循环语句作比较,那么循环语句称为循环,而其他的具体说明名称即可,所以这里要注意)操作来使得解决问题以及使得更加的简便即可,数学基本是用不上的
但是他的上限也只是数据结构的存放好坏的上限以及循环的上限,比如数组和链表,他们自身除了数据的存放结构外,得到的数据一般是使用循环,其中数组访问快,链表插入快,但他们的上限确固定了,你访问也只有这么快,插入也只有这么快,基本不能提高了,因为他们是写死的,大多数我们的程序都是操作下游的
而在上游(也就是问题困难或者基本需要考虑性能的)的情况下
下游的基本操作的优化可能满足不了需求了(基本到上限了),这时可能就需要使用数学了
这里为了让你更加的容易理解上游为什么可能需要数学的原因,这里给出一个简单的例子(只是简单的例子而已,就单纯的使用了数学,与实际情况需要使用数学的需求是差很多的,简单了解即可):
假设,我要你找出大于等于2的数是否是质数,你会怎么做?
质数:是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数
自然数:就是指大于等于0的整数
因数:是指整数a除以整数b(b≠0),即"a/b"的商正好是整数而没有余数(即余数为0),我们就说b是a的因数
比如6除以3,那么结果就是2,余数为0,那么3就是6的因数
你可能是如下操作:
代码如下(可以不看注释,只看代码,这是关于Scanner类的一些问题,了解即可):
import java.util.Scanner;

/**
 *
 */
public class test {
    public static void main(String[] args) {
        int i = 0;
        int j = 0;
        while (true) {
            j = 0;
            // 建议Scanner写在里面
            // 之所以这样写,而不用Scanner再次的获取(不操作同一个Scanner对象)
            // 这是为了防止Scanner数据缓存的问题(有缓冲区,放在缓冲区里的数据这里称为缓存),没有处理的话,会操作报错,这里会变成死循环的(因为while (true) )
            // 主要是因为第一次输入的数据如果报错的话
            // 他必须先处理才可以再次的操作你输入的数据(即你输入的数是不会读取的,但是还是存在的,有顺序的存在对应的缓冲区,可以被scanner.next()依次获取,有顺序的,谁先输入,那么先读取谁,虽然每次只能获取一个,你可以加上Thread.sleep(4000);来测试,测试代码后面会给出,自己测试就知道了)
            // 而由于这里是无限循环,那么一个报错,后续都会报错的(虽然这里是false,而不是操作catch报错或者直接报错,直接报错会结束循环的,这是必然的,除非是被解决的,如catch和后面的scanner.hasNextInt(),不同线程也行,所以通常在服务器里报错,并不会导致服务器停止,一般都会销毁,然后创建,所以报错在服务器里面通常没有影响)
            // 而创建新的对象就不会出现这样的问题了
            // 当然,若不是创建新的对象,但在他必经之路上(如catch里面或者后面的scanner.hasNextInt()的else里面)
            // 加上scanner.next()获取到也可以解决,即进行处理就行,否则这里会无限循环的
            Scanner scanner = new Scanner(System.in);
            // 获取一个整数,且只有整数才能为true,否则就是false
            // 如果返回false,虽然不是错误信息,但实际上缓存还是在的
            // 这里还是需要注意一下的,否则会一直返回false,而不会操作你输入数了(放在缓冲区准备处理)
            if (scanner.hasNextInt()) {
                i = scanner.nextInt();
            } else {
                System.out.println("请输入整数(需要是int范围的整数,不能大于等于2147483648,否则也会到这里)");
            }
            if (i < 2) {
                System.out.println("请输入大于等于2的数");
            }else{
                for(int a = 2;a<i;a++){
                    if(i%a==0){
                        System.out.println("不是质数");
                        j = 1;
                        break;
                    }
                }
                if(j == 0){
                    System.out.println("是质数");
                }
            }


            }
        }
    }

代码也的确解决了对应的问题,我们可以发现,他的解决方式
只是将自身依次的除以"2到自身减一"(包括2和自身减一)的所有数
只要有一个的余数为0,就代表是质数
我们发现,他的存储方式是赋值(数据结构),而解决问题的方式(算法)是利用循环操作
但是在数字越来越大时,他的执行时间,可能会变慢,你可以试着将值修改成2147483647,会发现,明显变慢了
接下来,我将使用一点数学知识或者说数学思维(虽然并不是很高大上,但也只是告诉你,数学的作用而已),来使得他进行优化
即我们需要减少循环,那么我们继续分析质数,有什么办法可以减少循环呢?
我们可以这样分析:如果一个数不是质数,那么必然是两个数的乘积
而一个数,也可以是他的两个平方根的乘积,比如9的平方根是3,那么3*3就是9
所以如果两个数其中有一个是大于等于平方根的,那么另外一个数是必然小于等于平方根的
而不可能大于平方根(因为如果是这样,那么他们的乘积,必然大于对应的数)
所以实际上,我们只需要将自身的数,依次的除以"2到数的平方根"(包括2和数的平方根)的所有数即可
那么,为什么不是依次的除以"数的平方根到自身减一"(包括数的平方根和自身减一)呢
实际上数字越大,“2到数的平方根”(包括2和数平方根)的所有数就越小,即依次的除以"2到数的平方根"的所有数,所需要的循环就越少,看y=x^2的图像就知道了,y的增长比x快多了
所以我们通常不会是依次的除以"数的平方根到自身减一"(包括数的平方根和自身减一)的所有数
而是依次的除以"2到数的平方根"(包括2和数的平方根)的所有数
那么我们的代码修改如下:
import java.util.Scanner;

/**
 *
 */
public class test {
    public static void main(String[] args) {
        int i = 0;
        int j = 0;
        while (true) {
            j = 0;
            Scanner scanner = new Scanner(System.in);
            if (scanner.hasNextInt()) {
                i = scanner.nextInt();
            } else {
                System.out.println("请输入整数(需要是int范围的整数,不能大于等于2147483648,否则也会到这里)");
            }
            if (i < 2) {
                System.out.println("请输入大于等于2的数");
            }else{
                //我们只需要整数,所以进行强转
                for(int a = 2;a<=(int)Math.sqrt(i);a++){  //这里进行了改变,如果有什么问题,你可以试着修改一下解决即可,总不能始终十全十美吧
                    if(i%a==0){
                        System.out.println("不是质数");
                        j = 1;
                        break;
                    }
                }
                if(j == 0){
                    System.out.println("是质数");
                }
            }


        }
    }
}

//实际上进行平方根自然也需要一定的时间,只是这些时间一般也相当于几个循环而已
//对比要循环很多次来说,是微不足道的,所以我们的确是优化了很多
为了验证是否优化,我们继续输入2147483647,可以很明显的发现,比之前的快了很多
所以在某些时候,数学的作用还是非常大的
最后,这里给出前面注释说明的测试代码(缓冲区的测试代码):
import java.util.Scanner;

/**
 *
 */
public class test2 {
    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);
        while (true) {
            try {
                Thread.sleep(4000); //多次输入就知道了
            }catch (Exception e){
                e.printStackTrace();
            }

            if (scanner.hasNextInt()) {
               int i = scanner.nextInt();
            } else {
                String next = scanner.next();
                System.out.println(next); //读取多次输入的其中一个,但是有顺序的,即缓冲区有顺序
                System.out.println("请输入整数(需要是int范围的整数,不能大于等于2147483648,否则也会到这里)");
            }

        }
    }
}

总结:
数据结构:就是数据在程序中的存放方式
无论你是赋值也好,还是使用数组或者链表也好,他们都是存放数据的方法,只是各有不同的结构
算法:解决问题的,无论你是容易的问题,还是困难的问题,只要是一个可以解决该问题的方式,就可以称为算法
所以在编程中,我们一般也认为:程序=算法+数据结构(所以他们是必然需要结合的),才能是一个程序
因为你一个程序,无非就是一个数据的存放(数据结构)以及运用(即使用算法来解决某些问题)等等的多个基本操作来完成的
在前面算法的下游中也有说明使用数据结构
常见算法:

在这里插入图片描述

算法复杂度:
数据结构和算法(必然是结合的)本质上就是"快"和"省"(即执行时间要慢,需要内存要少)
所以代码的执行效率是非常重要的度量,我们采用时间复杂度和空间复杂度来计算,一般来说,时间复杂度越低性能越高,当然,空间复杂度也能说明提高性能
因为性能一般指这个程序所需要的内存和时间的多少,我们大多数都认为是运行速度的快慢,实际上内存也算
时间复杂度:
大O复杂度表示法:
示例:
int sum(int n){
        int s=0; //t
        int i=1; //t
        for(;i<=n;i++){ //t*n
            s=s+i;      //t*n   
       }
        return s;       //t
   }
/*
t代表执行这一行代码(单纯的一行,而不是将所有代码放在一行中,看你自己理解了,通常我们认为是格式化操作后的一行代码,大多数都是这样认为的,具体我们认为是,某一种操作,即可能是多行,比如匿名内部类,也算一个t,主要看你自己的理解,大多数情况下,我们只会看多次执行的代码,比如循环,其他的都会进行忽略,后面会说明这种情况的)的时间,一般我们认为是1(无单位),在程序里我们是这样的,而不是时间的单位操作,因为这里是程序
我们假设t是1:
那么就是如下的执行行数:
1+1+n+n+1=2n+3(行代码) ,在n代表无限时,可以认为就是n,否则就是2n+3,后面会说明
    
    */

    //很明显,我们将执行的一行(条或者段)代码称为t,也就是称为1,无论是否是长的代码还是短的代码(表示一行的长短,需要是基础代码,而不能是循环,否则不称为1),都认为是1,而正是如此,所以实际上时间复杂度也只是大致的估算而已(即可能时间复杂度低的可能还要慢,后面会具体说明的),一般单纯的"}",以及方法名称本身不算(基本只算方法体),所以上面是五个t,但是由于循环的存在,对应的会执行n次,所以循环那里就是t*n

//时间复杂度越低越好,就比如O(n)比O(n^2)好
    
   
所以我们假设执行一行代码的时间为t,通过估算(即认为每一行代码都是t,所以只考虑估算,而不考虑有多长的代码)
很明显代码的执行时间T(n)与执行次数成正比,记做:
T(n)=O(f(n))
/*
T(n):代码执行时间,只是一个表示而已,即如果你顺眼,你可以认为只写上T,但是在数学上,我们一般是这样写的
比如f(x) = 2x+3等等,我们以这个为例
n:数据规模,相当于上面的2x中的x
f(n):每行代码执行次数总和,相当于上面的2x+3
通过f(x) = 2x+3可以知道,是成正比的,但是我们通常这样表示,即T(n)=O(f(n)),则认为代码的执行时间与f(n)表达式成正比
O(是大O,不是小o):代码的执行时间与f(n)表达式成正比,他只是一个符号,一般通常用O()来包括(包含)f(n)来表示,即O(f(n))来表示
无论这个正比的比例是多大,都用他来表示,即只要是正比就行,虽然也基本只有正比

上面的例子中的T(n)=O(2n+3)

当n无限大时,低阶、常量、系统,以及代码是否够长(即一行代码包括很多个代码,用";"分开的,比如:System.out.println(1);System.out.println(1);,当然了,必须是没有操作循环的代码,即基本代码,否则不能忽略了,当然这里也包括多少行,即上面的";"后面回车就是2行,即多行),都可以忽略,而正是因为可以设置无限大,所以我们通常将循环都看成n,无论该循环的次数是多少,就算是只执行两次,我们也看成n来操作时间复杂度
所以T(n)=O(n)
即上例中的时间复杂度为O(n),也就是与n成正比,也就是代码执行时间随着数据规模的增加而增长

所以,一般情况下,我们通常只会看循环或者说(及其)变化大(通常是最大,主要是这个)的代码来进行估算(时间复杂度),所以我们也尽量减少变化大来使得时间复杂度变低,因为这样是最有效的,正如前面说的,当n无限大时,低阶、常量、系统,以及代码是否够长,都可以忽略

虽然他们还是有差别,但我们的估算,就是进行忽略的估算,且随着n无限大时,还是正解的,所以也是最有效的
但是实际上,可能时间复杂度低的,可能也会慢,因为是估算,主要是忽略了代码长短,以及循环的次数导致的
比如一个代码里面,循环有2次,但是只有10行代码,而一个代码里面没有循环,但是有10000(或者更多)行代码(或者一行代码里面存放了他10000行的代码,因为有;来分开的),那么虽然后者是O(1),但是他确比前者的O(n)慢
所以在后期(如项目越来越大,代码越来越多,业务越来越多),该估算是越来越接近的,因为n通常可能会变得越来越大的,所以到那时,就会变成正解
所以我们对于时间复杂度来说,一般都是看后期的,所以这里我们以估算或者后期为主,以后都是如此
*/
再次举例:
int sum(int n){
        int s=0;
        int i=1;
        int j=1;
        for(;i<=n;i++){// n,实际上他可能执行了n+1次(因为=的存在),但是我们是估算的,所以认为是n次
            j=1;
            for(;j<=n;j++){ //n*n,估算就只看n^2了,(n+1)*(n+1) = n^2+2n+1 = n^2(估算)
                s=s+i+j;    //n*n
           }
       }
    //注意:并非一个循环就是n,在以后中,如果是指定条件,那么就看具体执行次数了,比如while,一般都不是n,但是for通常是,因为他基本是用来操作次数的,但无论是for还是while都要看具体次数,而不是通常认为,所以一个while或者一个for,并不是就是一个n,比如后面的二叉树
        return s;
   }

//很明显,直接就是T(n)=O(n*n),也就是代码执行时间随着数据规模的增加而平方增长(这里是),因为可能立方或者4次方等等,依次增加
//所以上例中的时间复杂度为O(n^2)

//正是因为n的增加,时间复杂度也越来越大
//所以时间复杂度也称为渐进时间复杂度

//这里提一下:实际上在程序里,乘法也是加法的累积操作,但是我们一般认为他也是O(1),所以时间复杂度,只是操作具体的明面上的次数,而不是底层的次数(基础底层),否则时间复杂度是非常大的,因为底层有很多操作,我们统一将底层称为O(1),当然,工具方法还是参照时间复杂度的,所以这里说的底层是,必须存在的基础底层,而不是他的扩展,比如String的类,虽然是自带的,但他的方法也是操作时间复杂度的,比如他重写的equals方法,以及自身的charAt方法等等,当然还有很多,就不依次说明了,一般的hashCode方法,即哈希函数,我们也会认为是基础的底层,即O(1),虽然String的哈希函数操作了循环,但我们也认为是O(1)


计算时间复杂度的技巧:
计算循环执行次数最多的代码
总复杂度=量级最大的复杂度,也就是之前说的:我们通常只会看循环或者说(及其)变化大(通常是最大,主要是这个)的代码来进行估算(时间复杂度)
比如把上面两段代码合在一起:
int sum(int n){
            int s=0;
            int i=1;
            int j=1;
            for(;i<=n;i++){ //t*n
                s=s+i;      //t*n   
           }
            for(;i<=n;i++){// n
                j=1;
                for(;j<=m;j++){ //n*m
                    s=s+i+j;    //n*m      这里表示n*m,这里认为是n==m(后面会说明不等于的时间复杂度),即认为是n*n,所以时间复杂度为O(n^2)
               }
           }
            return s;
       }

//时间复杂度为O(n^2),因为对于n^2来说,n可以忽略,也就是之前说的总复杂度=量级最大的复杂度
//即我们通常只会看循环或者说(及其)变化大(通常是最大,主要是这个)的代码来进行估算(时间复杂度)


//所以根据前面的案例可以得出,嵌套代码的复杂度等于嵌套内外代码复杂度的乘积(乘法法则)
常见的时间复杂度:
O(1)
这种是最简单的,也是最好理解的,就是常量级
不是只执行了一行代码,只要代码的执行不随着数据规模(n)的增加而增加,就是常量级
在实际应用中,通常使用冗余字段存储来将O(n)变成O(1)
比如Redis中有很多这样的操作用来提升访问性能
比如:SDS、字典、redis的跳跃表等,跳跃表一般情况下,都是O(logn)
redis跳跃表的除外,他是O(1),因为虽然名称相同,但是实现是不同的
具体的例子可以是:比如我们原本需要通过循环来取得对应的四个数来操作,如果我们不进行任何改变,那么下一次取得这四个数,必然也不是O(1),而是继续操作循环,那么可能是O(n)
但是,如果我们定义一个长度数组来获取这四个数,很明显,他一个人,就代表了多个数
那么下一次我们只需要在数组里取就行了,那么就变成O(1)了,这就是数据结构(好)会提高性能的一个示例
O(logn)、O(nlogn):
i = 1;
while(i <= n){
    i = i * 2;// 执行最多
}

//很明显,按照数学的方法和前面的估算可以得出,他是这样的认为:2^x = n
//所以我们认为是:logn = x(在时间复杂度里,一般不写底数,即忽略底数,因为对时间复杂度来说,默认n为无限大的,如果是1,那么就是n时间复杂度了,而不是logn时间复杂度了)
//这个x就可以认为是执行次数
//那么就是如下:T(n) = O(logn) = O(x),但是执行的次数是用n来表示的,所以这就是O(logn)的由来

//而如果外面嵌套了n,那么就是O(nlogn)时间复杂度了
//而如果是同等级的,即不是嵌套的,那么通常由于n是最大变化,那么一般就是O(n)时间复杂度了

//而由于logn的函数图像对比n来说,是在下面的,所以通常比n的时间复杂度要慢
//即我们可以将y=n和y=logn,这个y就代表执行次数(他由n来表示的),随着n变大,y(执行测试,T(n))变大(随函数图像变化)
快速排序、归并排序的时间复杂度都是O(nlogn)
O(n):
这个前面已经讲了,很多线性表的操作都是O(n),这也是最常见的一个时间复杂度
比如:数组的插入删除、链表的遍历等
O(m+n):
代码的时间复杂度由两个数据的规模来决定
int sum(int m,int n){
        int s1=0;
        int s2=0;
        int i=1;
        int j=1;
        for(;i<=m;i++){
            s1=s1+i; // 执行最多

       }
        for(;j<=n;j++){
            s2=s2+j; //执行最多

       }
        return s1+s2;
   }

//上面是n不等于m,所以时间复杂度是O(n+m),因为n和m都能无限大,所以不能忽略,而正是如此(都能无限大),所以我们也将他们称为相同提升(即他们对比不能忽略系数等等,其他的单独常数可以忽略,若是与无限大相关的特殊常数,比如我们越大,你也越大,只是提升非常小而已,也算),即虽然他们都是无限大,但是可能还是有差别的,比如n=2m,那么虽然n和m都是无限大,但反正m需要的时间少,因为n=2m,不能忽略系数(系数可以认为是有多少相加,比如有2个m相加,即2m,即2就是系数)了,所以在相同提升之间,除了无限大外,规模数据的提升速度也要进行判断
//即m和n是代码的两个数据规模,而且不能确定谁更大,此时代码的复杂度为两段时间复杂度之和
//即T(n)=O(m+n),记作:O(m+n)


O(m*n):
 int sum(int m,int n){
        int s=0;
        int i=1;
        int j=1;
        for(;i<=m;i++){// m
            j=1;
            for(;j<=n;j++){ //m*n
                s=s+i+j;    //m*n

           }
       }
        return s;
   }

//根据乘法法则代码的复杂度为两段时间复杂度之积,即T(n)=O(m*n),记作:O(m*n)
//当m==n时,为O(n^2),即统称为n,而不是m,这是这里的规定,当然你看m顺眼,你也可以认为是m^2,就如我们自己定义未知数一样,大多数是定义x,但也可以定义b,z等等
//当m==1时,为O(n),这是最好的情况
以后我们操作的时间复杂度,默认为是操作无限大的,所以该省略的就省略,来表示时间复杂度
空间复杂度:
空间复杂度全称是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系
比如将一个数组拷贝到另一个数组中,就是相当于空间扩大了一倍(由原来的n变成2n):T(n)=O(2n),估算即忽略系数
即为:O(n),这是一个非常常见的空间复杂度,比如跳跃表、hashmap的扩容
此外还有:O(1),比如原地排序
还有O(n^2),但是此种占用空间通常是非常大的,所以最好不操作,这里就不说明了,可能不会测试
由于现在硬件相对比较便宜,所以在开发中常常会利用空间来换时间,比如缓存技术
即不用使用时间再次的获取数据了,直接取出就行,持久化也算一个例子
典型的数据结构中空间换时间是:跳跃表
他的空间复杂度O(n),而对于查询来说,时间复杂度一般是O(logn),而不是平常数组的O(n)了,相当于取一半(二分,类似与后面的二分法,只是这里先操作好了,且通常不是一半,但思想与二分法基本一样的,来操作区间或者排除),具体可以看这个博客:https://blog.csdn.net/weixin_40413961/article/details/102306001
在实际开发中我们也更关注代码的时间复杂度,而用于执行效率的提升,而不会关注空间复杂度,所以这里以时间复杂度为主
为什么要学习数据结构和算法:
互联网行业中数据结构和算法尤为重要
互联网软件特点:高并发、高性能、高扩展、高可用、海量数据
能够更好的使用类库
对编程的追求,精益求精
数据结构与算法基础:
后面的代码中,最后通常会给出对应数据结构操作的完整代码,但是在借鉴之前,自己最好先写上一次
接下来我们来具体学习,在这之前,对应的时间复杂度是以我写的程序来进行说明的,当然,如果可以优化的话,自然是可以更加的减低时间复杂度,这里以对应的程序为主,所以你可能在其他博客或者百度里面可以发现,具体理论上的时间复杂度比我说明的时间复杂度可能要低,这是很正常的,因为这里的程序也可能并不是最优解,所以如果后面的说明中,出现了时间复杂度在理论上不符合,那么说明程序还可以继续优化,你可以自己进行操作优化
线性表(先说明这个),如:数组,链表等等
后续会说明非线性表,如:图,二叉树等等
线性表(Linear List)就是数据排成像一条线一样的结构
数据只有前后两个方向(为什么说前后呢,而不说左右呢,因为线也可以竖起来的,如栈,而前后能统称,所以说前后,即既可以表示栈也可以表示数组等等数据结构了,他们都可以称为线性表)

在这里插入图片描述

数组:
概念:
数组(Array)是有限个相同类型的变量所组成的有序集合,数组中的每一个变量被称为元素
数组是最为简单、最为常用的数据结构

在这里插入图片描述

数组下标从零开始(Why,规定的,就如1+1规定等于2,而不是他本身如此,因为数字就是发明用来便于计算的,后面会具体说明为什么从0开始)
存储原理:
数组用一组连续的内存空间来存储一组具有相同类型的数据

在这里插入图片描述

模拟内存存储:
灰色格子:被使用的内存空间
橙色格子:空闲的内存空间
红色格子:数组占用的内存空间
如果本来最后一个2的格子是灰色的
那么他们整体都需要取橙色的格子里找到一个可以存放他们连续的数据的地方,正好第三行就行
所以我们的数组也规定,自己不能进行扩容,需要其他数组才行,因为后面可能有其他的格子(如灰色的格子)
数组可以根据下标随机访问数据
比如一个整型数据 int[] 长度为5

在这里插入图片描述

假设首地址是:1000,一个内存空间虽然是一个格子,但是他这个格子大小可能是不同的,比如他这里占用4个字节,但是可能在其他地方是占用多个的,所以实际上如果不看格子的话,那么就看后面的数字也行,只是格子易观察,上面我们认为都是相同的格子,实际上是不是的,这里注意即可,在后面的链表中,说明内存空间时,就知道了
int是4字节(32位),所以上面后面的范围就是存储的哪四个字节
实际内存存储是位(显示出来的,也就是直观的,只是4容易表示,所以上面以4为主,一般int位数值是对应几次方的范围,即 -2^31 ~ 2^ 31-1)
随机元素寻址:
a[i]_address=a[0]_address+i*4
    //比如i为2,那么开头的1008-1011这四个字节
    //程序上通常没有这样的表示,这里只是用来解释上面的操作的
    
    //因为这里是直接算出来的,所以时间复杂度是O(1)
该公式解释了三个方面:
1:连续性分配(为了可以计算,因为保存有首地址,即a[0]_address,所以我们只需要改变"i"即可,即下标)
即内存空间是连续的,所以1003后面就是1004
2:相同的类型
3:下标从0开始,这里也能说明,为什么规定从0开始了,因为从0开始就是首地址,否则就不是了
实际上就是因为上面的表达式造成的从0开始,如果i * 4变成了(i-1)* 4,那么自然,就是从1开始的
可是这里没有,所以是从0开始,除非你能改变他的底层代码,使得他从1开始,如果可以,只需要改变他(i-1)* 4即可
因为超过范围的,也通常就会出现数组越界的异常,自然就使得从1开始了,因为1就是0了,这时1也表示首地址了
所以下标只是为了计算出对应的地址而已,而不是说他本来就规定是该下标的
操作:
读取元素:
根据下标读取元素的方式叫作随机读取
int n=nums[2]
更新元素:
nums[3]= 10;
注意不要数组越界(即对应的地址不存在),否则会报错的
读取和更新都可以随机访问,时间复杂度为O(1)
插入元素:
有三种情况:
1:尾部插入:
在数据的实际元素数量小于数组长度的情况下,直接把插入的元素放在数组尾部的空闲位置即可,等同于更新元素的操作

在这里插入图片描述

a[6]=10 //等同于更新,因为有默认的0这个值
中间插入(包含首部插入,因为是连续的,所以首部实际上也算中间):
在数据的实际元素数量小于数组长度的情况下:
由于数组的每一个元素都有其固定下标,所以首先把插入位置及后面的元素向后移动,腾出地方(有时候是最后的一个数先移动,否则可能会被覆盖的,或者使用第三方数来保存,那么可以让插入的地方先移动,我们通常使用第三方数来操作,因为正向的循环对大多数人比较友好,即从小到大的加,而不是从大到小的减,这是大多数人的习惯,除了某些习惯从大到小的人群)
上面不同的方式,也是算法的一种体现
再把要插入的元素放到对应的数组位置上

在这里插入图片描述

超范围插入:
假如现在有一个数组,已经装满了元素,这时还想插入一个新元素,或者插入位置是越界的
这时就要对原数组进行扩容:可以创建一个新数组,长度是旧数组的2倍
再把旧数组中的元素统统复制过去,这样就实现了数组的扩容

在这里插入图片描述

int[] numsNew=new int[nums.length*2];
System.arraycopy(nums,0,numsNew,0,nums.length);
// 原数组就丢掉了(虽然只是换一个数组),因为资源浪费(一般垃圾回收机制会回收没有被变量指向的内存空间,主要是这个,以及基本不使用的变量,这个通常不考虑,在java中通常不能手动操作内存,而c或者c++确可以)
nums=numsNew;
//他们都指向同一个数组,但是我们的数组还是增大了空间的,且多一个变量
删除元素:
数组的删除操作和插入操作的过程相反,如果删除的元素位于数组中间,其后的元素都需要向前挪动1位
for(int i=p;i<nums.length;i++){
            nums[i-1]=nums[i];
}
//如果是尾部删除,那么直接设置为0即可,即默认值,我们也将尾部默认值认为是删除的(这只是普遍认为的,他还是存在的)
//且无论是否是我们添加的值,在数组中,只要尾部是默认的值(我们在尾部添加0这个数或者说默认值也算,中间默认值是不算的,都认为是没有删除的,除了尾部的默认值),我们都认为没有操作,即一般认为是删除的(虽然他还在,但这只是普遍认为而已)
时间复杂度:
读取(单纯的读取,而不是全部读取,即查询,这里之所以也是O(1),在后续的链表中可以具体了解)和更新都是随机访问,所以是O(1)
插入数组并扩容(也是循环的操作,前面已经说明过了,循环是多次的操作,而不只是对应的循环语句,如不只是for循环语句)的时间复杂度都是O(n),插入并移动元素的时间复杂度也是O(n)
综合起来插入操作的时间复杂度是O(n), 删除操作,也会涉及元素的移动,所以时间复杂度也是O(n)
很明显,操作循环的,基本都是O(n),因为对应的循环可以无限大(因为n的存在,他这个对应位置或者说循环是可以变化的)
优缺点:
优点: 数组拥有非常高效的随机访问能力,只要给出下标,就可以用常量时间找到对应元素
缺点: 插入和删除元素方面,由于数组元素连续紧密地存储在内存中
插入、删除元素都会导致大量元素被迫移动,影响效率(类似于ArrayList,他是封装数组的,且有对应已经封装好的具体方法,比如添加或者删除等等,所以他可以说是上面说明的数组的优缺点,而LinkedList在这插入和删除方面比较有效率)
申请的空间必须是连续的,也就是说即使有空间也可能因为没有足够的连续空间而创建失败
如果超出范围,需要重新申请内存进行存储
原空间就容易浪费了(虽然垃圾回收机制可以删除该内存,但是没删除之前还是被占用的)
应用:
数组是基础的数据结构,应用太广泛了,ArrayList、Redis、消息队列等等
数据结构和算法的可视化网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
现在,你自己要求自己写出对应数组的增删改查的方法,并测试
这里给出对应数组的全部基本操作代码(记得自己写一下,然后再看哦):
/**
 *
 */

//可以使用shift+ctrl+alt+l,然后在弹出的框框中点击Run或者回车,即可格式化代码,这是大多数idea自带的功能
public class Array {
    int array = 8;
    int[] nums = new int[array];
    // 成员变量可以互相使用,前提是该变量要在前面先加载,否则启动会报错
    // 除非他是静态的,但是若这里也是静态的,那么也需要在前面,否则启动也会报错
    // 即静态(包括静态块,但是静态块放在前面,然后再使用后面的变量时不会报错,只是不会使用而已)之间也需要有顺序,即也要放在前面

    public Array() {
        //这里我们就定义一下默认数据吧,否则都会默认是0的
        nums[0] = 3;
        nums[1] = 1;
        nums[2] = 2;
        nums[3] = 5;
        nums[4] = 4;
        nums[5] = 9;
    }

    //得到指定下标的数组内容
    public int get(int i) {
        if (i < 0 || i >= array) {
            System.out.println("下标越界了");
            return -1; //获得-1代表错误
        }
        return nums[i];
    }

    //修改指定下标的内容
    public void update(int i, int n) {
        if (i < 0 || i >= array) {
            System.out.println("下标越界了");
            return;
        }
        nums[i] = n;
    }

    //修改下标为6的内容(值)
    public void insert6(int n) {

        nums[6] = n;
    }

    // 这里使用从大到小来操作的,将后续都进行移动
    // 但是很明显,如果最后一个下标有值(认为不是默认值),就进行了扩容
    public void insert(int p, int n) {
        if (p < 0 || p >= array) {
            System.out.println("下标越界了");
            return;
        }

        for (int i = nums.length - 2; i >= p; i--) {

            nums[i + 1] = nums[i];

        }
        nums[p] = n;

        //操作在指定下标的位置下插入数据,然后移动数据
        if (nums[nums.length - 1] != 0) { //有值(就是默认值)则进行扩容,这是为了保证不被覆盖,因为没扩容的话,那么就只有更新覆盖咯
            resize();

        }
    }

    //将数组进行扩容两倍
    public void resize() {
        int[] numsNew = new int[nums.length * 2];
        System.arraycopy(nums, 0, numsNew, 0, nums.length);
        nums = numsNew;
    }

    //删除指定下标的值,然后移动数据
    public void delete(int p) {
        if (p < 0 || p >= array) {
            System.out.println("下标越界了");
            return;
        }
        for (int i = p; i < nums.length - 1; i++) {
            nums[i] = nums[i + 1];
        }
        nums[nums.length - 1] = 0;
    }

    //打印数组中的所有数据
    public void select() {
        String a = "[";
        for (int j = 0; j < nums.length; j++) {
            if (j == nums.length - 1) {
                a = a + nums[j] + "]";
            } else {
                a = a + nums[j] + ",";
            }
        }

        System.out.println(a);
    }

    //反过来打印数组中的所有数据
    public void reverseSelect() {
        String a = "[";
        for (int j = nums.length - 1; j >= 0; j--) {
            if (j == 0) {
                a = a + nums[j] + "]";
            } else {
                a = a + nums[j] + ",";
            }
        }
        System.out.println(a);
    }


    //根据测试,发现并没有什么问题(如果有某些问题,你可以试着修改一下),至此数组操作完毕
    public static void main(String[] args) {
        Array d = new Array();
        d.insert(6, 3); //在下标6这个位置插入数据
        d.insert(7, 3); //在下标7这个位置插入数据
        d.insert(8, 6);
        //查询数据
        d.select();
        d.reverseSelect();
        System.out.println(d.get(1)); //获取下标为1的数据
        d.delete(6); //删除下标为6的数据
        d.update(1, 32); //修改下标为1的值为32
        d.select();
        d.reverseSelect();
        d.resize();
        d.select();
        d.insert6(36);
        d.select();


    }
}



至此,数组操作完毕
链表 :
概念:
链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成
链表中数据元素的逻辑顺序是通过链表中的指针链接次序实现的
链表由一系列结点(链表中每一个元 素称为结点,或者节点)组成,结点可以在运行时动态生成
每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域(百度百科)
常见的链表包括:单链表、双向链表、循环链表
单链表:
单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next

在这里插入图片描述

Node{
    int data;
    Node next;    
}
双向链表:
双向链表的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针

在这里插入图片描述

Node{
    int data;
    Node next;    
    Node prev;
}
循环链表:
链表的尾节点指向头节点形成一个环,称为循环链表

在这里插入图片描述

存储原理:
数组在内存中的存储方式是顺序存储(连续存储)
链表在内存中的存储方式则是随机存储(链式存储),只要有空闲的内存空间位置就能存储,该空间格子大小不是认为是4字节哦
而是各自类的字节数,他们实际上通常是不一样的内存空间大小的,只是为了方便观察,所以认为都是同样的空间,所以使用图片表示,以后说明这样时,都按照这样的思想来说明
链表的每一个节点分布在内存的不同位置,依靠next指针关联起来,这样可以灵活有效地利用零散的碎片空间,这也是不使用数组,而使用链表的好处之一

在这里插入图片描述

链表的第1个节点被称为头节点(3),即我们也通常将他的地址称为首地址,没有任何节点的next指针指向它,或者说它的前置节点为空头结点用来记录链表的基地址
有了它,我们就可以遍历得到整条链表,链表的最后1个节点被称为尾节点(2),它指向的next为空
操作(以单链表为主,所以只给出单链表相关图片了):
查找节点:
在查找元素时,链表只能从头节点开始向后一个一个节点逐一查找

在这里插入图片描述

更新节点:
找到要更新的节点,然后把旧数据替换成新数据

在这里插入图片描述

插入节点:
尾部插入:
把最后一个节点的next指针指向新插入的节点即可

在这里插入图片描述

头部插入
第1步,把新节点的next指针指向原先的头节点
第2步,把新节点变为链表的头节点(一般有个变量专门记录头节点的地址的,且必须指定头节点,而不能是指定其他节点,因为在程序里操作,是从头节点开始的,否则怎么操作数据完整呢,且无论该头地址是否发生改变,都用它来记录头地址,因为总要有个记录头地址的变量吧,要不然怎么操作呢,所以我们通常会定义一个头节点的引用变量在类里面,然后我们可以操作该变量来相当于操作头节点地址,就如int i = 1,通过操作"i"变量来操作"1"这个值,这是肯定的,即是基础知识)

在这里插入图片描述

中间插入
第1步,新节点的next指针,指向插入位置的节点
第2步,插入位置前置节点的next指针,指向新节点

在这里插入图片描述

只要内存空间允许,能够插入链表的元素是无限的,不需要像数组那样考虑扩容的问题,这也是随机存储的好处
删除节点:
尾部删除:
把倒数第2个节点的next指针指向空即可,一般垃圾回收机制会回收没有被变量指向的内存空间(主要是这个)以及基本不使用的变量,这个通常不考虑,变量也会占用空间的,实际上无论什么东西基本都要空间
因为垃圾回收机制的存在,所以总体来说,我们不用考虑对应的内存空间是否释放的问题

在这里插入图片描述

头部删除:
把链表的头节点设为原先头节点的next指针即可,那么就是变量指向变化了,所以会垃圾回收机制会回收原来头节点内存空间

在这里插入图片描述

中间删除:
把要删除节点的前置节点的next指针,指向要删除元素的下一个节点即可

在这里插入图片描述

现在,你自己要求自己写出对应链表的增删改查的方法,并测试
这里给出对应链表(单链表)的全部基本操作代码(粗略的代码,记得自己写一下,然后再看哦):
/**
 *
 */
public class test {

    //根据前面的说明,我们需要定义一个头节点的引用变量在类里面进行操作,当然这里可以设置为null(使用默认即可),因为添加操作里进行了解决
    Node head;

    //定义一个内部类来使用
    public class Node {
        int id;
        String name;
        Node next;

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


    }

    //插入一个节点,包含多种插入,前面已经说明过了,但是这里与前面数组不同的是,这里基本是没有下标的
    //所以表示首部和尾部有点困难,所以我们这里以变量id的值的大小进行排列(大的放在后面,小的放在前面,相同的则覆盖)
    //这里仍然使用单独的方法来进行操作

    //根据id大小(大的放在后面,小的放在前面,相同的则覆盖)来插入节点
    public void insert(Node node) {
        //在前面我们说过,指向头节点地址的变量不能指向其他节点(因为要保证整体性),所以会有如下
        //由于在if里面为true时,可能需要使用头节点里面的信息,所以这里为了保证头节点的变量不被赋值,即定义一个临时变量
        Node temp1 = head;
        //由于在后续操作中,我们需要一个变量来保存temp变化的(即next的上一个,因为后面有temp1 = temp1.next;,重新赋值了,那么需要temp2 = temp1;,即temp2就是他赋值之前的,即temp1的上一个节点)上一个节点(因为一个变量基本只能表示一种,即基本不可以既表示当前,又表示上一个),所以也需要定义一个临时变量
        Node temp2 = head;
        //所以说,一个临时变量还是非常有用的,能够来操作保证某个变量不被赋值的情况
        //实际上我们只是操作他们的内容,即他们内容之间的联系,而不会操作直接的变量赋值,因为这样会使得指向发生改变
        //而正是因为操作内容,所以我们只是改变内容的指向,而我们的变量只是用来操作这个指向而已
        //所以我们在最后赋值时,通常都不是直接的赋值给临时变量,而是赋值给里面的内容(否则就是改变临时变量的指向了)
        //因为操作临时变量也就相当于操作其对象的
        //所以才可以操作如下的链表
        if (temp1 != null) {
            while (true) {
                if (temp1.id < node.id) {
                    //实际上这个地方只是要temp1即可,即将temp1 = temp1.next;放在后面,而if (temp1 == null) {修改成if (temp1.next == null) {,这样也行,但是有temp2使用,为什么就不使用呢,你说是吧,但是这里还真的不能改变位置,因为是用来保证他们是不同的指向的,使得temp2是temp1的上一个节点,或者说,使得temp2.next = temp1,这是为例后面的操作,即if (temp1.id > node.id) {,来保证放入他们两个中间
                    temp2 = temp1; //保留上一个节点
                    temp1 = temp1.next; //重新赋值给自己的下一个来进行判断,使得循环时,是判断他的
                    if (temp1 == null) {
                        temp2.next = node; //这里熟练的使用了地址哦
                        //不用使用break,也可以使得不执行,这里直接结束方法,而不执行后续代码
                        //所以大多数情况下若认为break后面没有其他操作时,就使用return;
                        return;
                    }
                }
                //我们尽量不用else,因为else太过绝对,且后期不好维护(扩展方面),即不好加条件(因为条件要不满足前面的所有if),以后基本是不加else了,除非基本固定的或者基本不用维护的,那么为了代码变少,可以使用,当然,只使用if之前,由于前面的if,可以不满足,所以需要考虑前面的判断是否会影响后面的if,这是主要的,所以在这个方面,只使用if也不好维护,因为容易出错,即if之间通过不能有过节,所以一般会加上返回break或者return;来结束,或者里面的代码互不相关,即if虽然不限制条件,但是也要考虑影响情况,简单来说,else比较稳定不会出错,但是不好扩展,而if不稳定,容易出错,但是好扩展,所以他们各有利弊吧,这里我们就不加else了,来试一下特殊的操作吧
                if (temp1.id > node.id) {
                    node.next = temp1;
                    if (temp1 != head) {
                        //如果不是最小的,就放入中间
                        temp2.next = node;
                        return;
                    }
                    head = node; //如果是最小的,那么就变成头节点
                    return;
                }
                //这里我们并不操作name的变化,而是整个对象进行覆盖,更难哦
                if (temp1.id == node.id) {
                    if (temp1 == head) {
                        node.next = temp1.next;
                        head = node;

                        return;
                    }
                    if (temp1.next != null) {
                        temp2.next = node;
                        node.next = temp1.next;
                        return;
                    }
                    //这里之所以可以不加if判断,是因为前面的判断基本都会结束方法,所以这里可以不加
                    temp2.next = node;
                    return;
                }
            }
        }

        head = node; //没有节点的话,就直接添加

    }

    //根据id删除指定节点,基本没有不同的,因为相同的操作了覆盖
    public void delete(int id) {
        Node temp1 = head;
        Node temp2 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) { 
                    temp2 = temp1;
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        System.out.println("没有删除的节点");
                        return; //没有删除的节点
                    }
                }
                if (temp1.id == id) {
                    if (temp1 != head) {
                        temp2.next = temp1.next;
                        return;
                    }

                    head = head.next;
                    return;
                }

            }
        }

        System.out.println("没有节点删除了");
    }


    //根据id修改指定节点的name
    public void update(int id, String name) {
        Node temp1 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) {
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        System.out.println("没有修改的节点");
                        return; //没有修改的节点
                    }
                }
                if (temp1.id == id) {
                    temp1.name = name;
                    return;
                }

            }
        }
        System.out.println("没有节点更新了");

    }

    //根据id查询指定节点,一般来说链表是不能这样的,只能通过下标来获取,因为id实际上也是数据,这样的话,相当于key和value的模式了,即散列表了,虽然他没有操作哈希值,所以这个可以看成是一个扩展,因为覆盖的原因,所以也能这样操作
    public void selectByNode(int id) {
        Node temp1 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) {
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        System.out.println("没有查询的节点");
                        return; //没有查询的节点
                    }
                }
                if (temp1.id == id) {
                    System.out.println("[" + temp1.id + "," + temp1.name + "]");
                    return;
                }
            }
        }
        System.out.println("没有节点查询了");

    }


    //查询所有节点
    public void select() {
        //在String中,null会看成字符串null来进行拼接的,比如a+1,那么结果就是null1,所以这里用""初始化,而不是null初始化
        String a = "";
        Node temp1 = head;
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + "[" + temp1.id + "," + temp1.name + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp1.id + "," + temp1.name + "],";

            while (true) {

                temp1 = temp1.next;
                if (temp1.next == null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "]";
                    break; //这里就加上break,因为我们需要结合后面的[]打印,当然这里也可以独自打印,只是会多出一行打印代码而已
                }
                if (temp1.next != null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "],";
                }


            }
        }
        if (temp1 == null) {
            a = "[]";
        }


        System.out.println(a);
    }

    //反过来查询所有节点,由于不是双向链表,所以这里需要进行特殊改动,这里我们的改动是,定义两个字符串来解决
    public void reverseSelect() {
        String a = "";
        String b = "";
        Node temp1 = head;
        String cc = "";
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + temp1.id;
                b = b + temp1.name;
                System.out.println("[" + a + "," + b + "]");
                return;
            }
            a = a + temp1.id + ",";
            b = b + temp1.name + ",";
            while (true) {


                temp1 = temp1.next;

                if (temp1.next == null) {
                    a = a + temp1.id;
                    b = b + temp1.name;
                    break;
                }
                if (temp1.next != null) {
                    a = a + temp1.id + ",";
                    b = b + temp1.name + ",";
                }


            }
            String[] aa = a.split(",");
            String[] bb = b.split(",");
            for (int j = aa.length - 1; j >= 0; j--) {
                if (j == 0) {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "]";
                } else {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "],";
                }

            }
        }
        if (temp1 == null) {
            cc = "[]";
        }


        System.out.println(cc);
    }


    //通过测试,发现基本没有什么问题(如果有某些问题,你可以试着修改一下),至此单链表操作完毕
    public static void main(String[] args) {
        test t = new test();
        //添加节点
        t.insert(t.new Node(26, "2"));
        t.insert(t.new Node(2, "2"));
        t.insert(t.new Node(2, "24"));
        t.insert(t.new Node(-1, "2"));
        //查询数据
        t.select();
        t.reverseSelect();
        //删除id为2的节点(若有多个相同的id,则删除前一个)
        t.delete(2);
        t.select();
        t.reverseSelect();
        //更新id为26的节点的name为5
        t.update(26, "5");
        t.select();
        t.reverseSelect();
        t.selectByNode(2);
        t.selectByNode(26);
    }
}

至此,单(向)链表操作完毕,接下来我们来完成双向链表(记得自己写一下,然后再看哦):
/**
 *
 */
public class test1 {

    //根据前面的说明,我们需要定义一个头节点的引用变量在类里面进行操作,当然这里可以设置为null(使用默认即可),因为添加操作里进行了解决
    Node head;

    //定义一个内部类来使用
    public class Node {
        Node prev;
        int id;
        String name;
        Node next;

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


    }

    //根据id大小(大的放在后面,小的放在前面,相同的则覆盖)来插入节点
    public void insert(Node node) {
        Node temp1 = head;
        Node temp2 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id < node.id) {
                    temp2 = temp1;
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        temp2.next = node;
                        node.prev = temp2;
                        return;
                    }
                }
                if (temp1.id > node.id) {
                    node.next = temp1;
                    temp1.prev = node;
                    if (temp1 != head) {

                        temp2.next = node;
                        node.prev = temp2;
                        return;
                    }
                    head = node;
                    return;


                }
                if (temp1.id == node.id) {
                    if (temp1 == head) {
                        node.next = temp1.next;
                        temp1.next.prev = node;
                        head = node;
                        return;
                    }
                    if (temp1.next != null) {
                        temp2.next = node;
                        node.next = temp1.next;
                        node.prev = temp2;
                        return;
                    }
                    temp2.next = node;
                    node.prev = temp2;
                    return;
                }

            }
        }
        head = node;

    }

    //根据id删除指定节点
    public void delete(int id) {
        Node temp1 = head;
        Node temp2 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) {
                    temp2 = temp1;
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        System.out.println("没有删除的节点");
                        return; //没有删除的节点
                    }
                }
                if (temp1.id == id) {
                    if (temp1 != head) {
                        temp2.next = temp1.next;
                        temp1.next.prev = temp2;
                        return;
                    }
                    head = head.next;
                    return;
                }
            }
        }
        System.out.println("没有节点删除了");

    }

    //根据id修改指定节点的name
    public void update(int id, String name) {
        Node temp1 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) {
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        System.out.println("没有修改的节点");
                        return; //没有修改的节点
                    }
                }
                if (temp1.id == id) {
                    temp1.name = name;
                    return;
                }
            }
        }
        System.out.println("没有节点修改了");

    }

    //根据id查询指定节点
    public void selectByNode(int id) {
        Node temp1 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) {
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        System.out.println("没有查询的节点");
                        return; //没有查询的节点
                    }
                }
                if (temp1.id == id) {
                    System.out.println("[" + temp1.id + "," + temp1.name + "]");
                    return;
                }
            }
        }
        System.out.println("没有节点查询了");

    }


    //查询所有节点
    public void select() {
        String a = "";
        Node temp1 = head;
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + "[" + temp1.id + "," + temp1.name + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp1.id + "," + temp1.name + "],";

            while (true) {

                temp1 = temp1.next;

                if (temp1.next == null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "]";
                    break;
                }
                if (temp1.next != null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "],";
                }


            }
        }
        if (temp1 == null) {
            a = "[]";
        }
        System.out.println(a);
    }

    //反过来查询所有节点,由于是双向链表,所以这里不需要进行特殊改动,看如下操作:
    public void reverseSelect() {

        Node temp1 = head;
        Node temp2 = null;
        String a = "";
        if (temp1 != null) {

            //首先获取最后一个节点
            while (true) {
                temp1 = temp1.next;
                if (temp1 == null) {
                    temp2 = head;
                    break;
                }
                if (temp1.next == null) {
                    temp2 = temp1;
                    break;
                }
            }

            if (temp2.prev == null) {
                a = a + "[" + temp2.id + "," + temp2.name + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp2.id + "," + temp2.name + "],";

            //根据最后一个节点来反过来查询即可
            while (true) {
                temp2 = temp2.prev;
                if (temp2.prev == null) {
                    a = a + "[" + temp2.id + "," + temp2.name + "]";
                    break;
                }
                if (temp2.prev != null) {
                    a = a + "[" + temp2.id + "," + temp2.name + "],";
                }


            }
        }
        if (temp1 == null) {
            a = "[]";
        }
        System.out.println(a);


    }

    //通过测试,发现基本没有什么问题(如果有某些问题,你可以试着修改一下),至此双向链表操作完毕
    public static void main(String[] args) {
        test1 t = new test1();
        //添加节点
        t.insert(t.new Node(26, "2"));
        t.insert(t.new Node(2, "2"));
        t.insert(t.new Node(2, "24"));
        t.insert(t.new Node(-1, "2"));
        //查询数据
        t.select();
        t.reverseSelect();
        //删除id为2的节点(若有多个相同的id,则删除前一个)
        t.delete(2);
        t.select();
        t.reverseSelect();
        //更新id为26的节点的name为5
        t.update(26, "5");
        t.select();
        t.reverseSelect();
        t.selectByNode(2);
        t.selectByNode(26);
    }
}

至此,双向链表操作完毕,接下来我们来完成循环链表(记得自己写一下,然后再看哦):
/**
 *
 */
public class test2 {

    //根据前面的说明,我们需要定义一个头节点的引用变量在类里面进行操作,当然这里可以设置为null(使用默认即可),因为添加操作里进行了解决
    Node head;

    //定义一个内部类来使用
    public class Node {
        int id;
        String name;
        Node next;

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


    }

    //从这里我们需要进行大致的具体改变了,因为这里没有一个明确的最后一个节点
    //也就是说,添加,删除,修改,查询中的循环,需要判断是否到达最后一个,因为这里是循环链表的操作

    //由于大多数操作可能需要获取最后一个节点,来完成将头节点赋值给最后一个节点的next
    //所以这里编写一个方法专门这样操作
    public Node last() {
        Node temp1 = head;
        while (true) {
            temp1 = temp1.next;
            //这里不需要上一个节点,所以就这样做了,这里最好将temp1 = temp1.next;放在if语句后面来使得逻辑通顺,虽然这里也并没有什么问题
            //这里不需要考虑空指针,因为就算是一个节点,他也是指向自己的,当然这里并没有循环的意思,因为我们只是给出地址而言,即赋值地址给变量,所以不会有什么循环的问题,只是可以用他自身的变量代表自己
            //但也正是因为指向的问题,所以实际上变量的值,与对象是不在一个频道的,即不能认为是相等的,只是给出一个地址而言,那么当我们通过该变量来使得改变对象中,该变量的值为null时,他并不是操作变量来进行赋值,而是操作对象,所以不会有认为为什么赋值null,确是自己调用的原因,因为虽然是该变量调用,但实际上还是对象在操作
            
            if (temp1.next == head) { //就算temp1就是head也没有关系,反正他的next也是head

                return temp1;
            }
        }

    }

    //根据id大小(大的放在后面,小的放在前面,相同的则覆盖)来插入节点
    public void insert(Node node) {
        Node temp1 = head;
        Node temp2 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id < node.id) {
                    temp2 = temp1;
                    temp1 = temp1.next;
                    if (temp1 == head) { //循环链表需要这样操作,代表这里是最后一个
                        temp2.next = node;
                        node.next = head;
                        return;
                    }
                }
                if (temp1.id > node.id) {
                    node.next = temp1;

                    if (temp1 != head) {
                        //如果不是最小的,就放入中间
                        temp2.next = node;
                        return;
                    }
                    Node next = last();
                    head = node;
                    next.next = head;
                    return;
                    //循环链表需要这样操作,将最后一个指向,变成我们加在前面的节点
                    //因为这里是改变指向,而不是改变值,所以虽然添加时,是将head赋值给最后一个节点的next,但是这里并不是修改对象值
                    //而是改变了head的指向,所以最后一个节点虽然得到的是原来head的指向,但是并不会随着head的指向改变而改变,除非head是修改值的,那么对应的指向值也会变,因为是同一个指向
                    //这是内存指向的问题,是基础知识,就不多说明了


                }
                if (temp1.id == node.id) {
                    if (temp1 == head) {
                        node.next = temp1.next;
                        Node next = last();
                        head = node;
                        next.next = head;
                        return;

                    }
                    if (temp1.next != head) {
                        temp2.next = node;
                        node.next = temp1.next;
                        return;
                    }

                    temp2.next = node;
                    node.next = head;
                    return;
                }

            }
        }
        if (temp1 == null) {
            head = node;
            node.next = head;

        }
    }


    //根据id删除指定节点
    public void delete(int id) {
        Node temp1 = head;
        Node temp2 = head;

        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) {
                    temp2 = temp1;
                    temp1 = temp1.next;
                    if (temp1 == head) {
                        System.out.println("没有删除的节点");
                        return; //没有删除的节点
                    }
                }
                if (temp1.id == id) {
                    if (temp1 != head) {
                        if (temp1.next == head) {
                            temp2.next = head;
                            return;
                        }
                        temp2.next = temp1.next;
                        return;

                    }
                    if (temp1 == head) {
                        head = null;
                        return;
                    }

                }
            }
        }
        if (temp1 == null) {
            System.out.println("没有删除的节点了");
        }
    }

    //根据id修改指定节点
    public void update(int id, String name) {
        Node temp1 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) {
                    temp1 = temp1.next;
                    if (temp1 == head) {
                        System.out.println("没有修改的节点");
                        return; //没有修改的节点
                    }
                }
                if (temp1.id == id) {
                    temp1.name = name;
                    return;
                }
            }
        }
        if (temp1 != null) {
            System.out.println("没有修改的节点了");
        }
    }

    //根据id查询指定节点
    public void selectByNode(int id) {
        Node temp1 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.id != id) {
                    temp1 = temp1.next;
                    if (temp1 == head) {
                        System.out.println("没有查询的节点");
                        return; //没有查询的节点
                    }
                }
                if (temp1.id == id) {
                    System.out.println("[" + temp1.id + "," + temp1.name + "]");
                    return;
                }
            }
        }
        if (temp1 == null) {
            System.out.println("没有查询的节点了");
        }
    }


    //查询所有节点
    public void select() {
        String a = "";
        Node temp1 = head;
        if (temp1 != null) {
            if (temp1.next == head) {
                a = a + "[" + temp1.id + "," + temp1.name + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp1.id + "," + temp1.name + "],";

            while (true) {

                temp1 = temp1.next;

                if (temp1.next == head) {
                    a = a + "[" + temp1.id + "," + temp1.name + "]";
                    break;
                }
                if (temp1.next != head) {
                    a = a + "[" + temp1.id + "," + temp1.name + "],";
                }


            }
        }
        if (temp1 == null) {
            a = "[]";
        }
        System.out.println(a);
    }

    //反过来查询所有节点,由于不是双向链表,所以这里需要进行特殊改动,这里我们的改动是,定义两个字符串来解决
    public void reverseSelect() {
        String a = "";
        String b = "";
        Node temp1 = head;
        String cc = "";
        if (temp1 != null) {
            if (temp1.next == head) {
                a = a + temp1.id;
                b = b + temp1.name;
                System.out.println("[" + a + "," + b + "]");
                return;
            }
            a = a + temp1.id + ",";
            b = b + temp1.name + ",";
            while (true) {


                temp1 = temp1.next;
                if (temp1.next == head) {
                    a = a + temp1.id;
                    b = b + temp1.name;
                    break;
                }
                if (temp1.next != head) {
                    a = a + temp1.id + ",";
                    b = b + temp1.name + ",";
                }


            }
            String[] aa = a.split(",");
            String[] bb = b.split(",");
            for (int j = aa.length - 1; j >= 0; j--) {
                if (j == 0) {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "]";
                } else {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "],";
                }

            }
        }
        if (temp1 == null) {
            cc = "[]";
        }


        System.out.println(cc);
    }

    //通过测试,发现基本没有什么问题(如果有某些问题,你可以试着修改一下),至此循环链表操作完毕
    public static void main(String[] args) {
        test2 t = new test2();
        //添加节点
        t.insert(t.new Node(26, "2"));
        t.insert(t.new Node(2, "2"));
        t.insert(t.new Node(2, "24"));
        t.insert(t.new Node(-1, "2"));
        //查询数据
        t.select();
        t.reverseSelect();
        //删除id为2的节点(若有多个相同的id,则删除前一个)
        t.delete(2);
        t.select();
        t.reverseSelect();
        //更新id为26的节点的name为5
        t.update(26, "5");
        t.select();
        t.reverseSelect();
        t.selectByNode(2);
        t.selectByNode(26);

        //验证链表是否的确是循环链表,只需要看第一个节点的信息,然后对比一下打印信息就知道了,如果都相同,那么基本是循环链表了
        if (t.head != null) {
            System.out.println("最后一个节点的next的id是:" + t.last().next.id);
            System.out.println("最后一个节点的next的name是:" + t.last().next.name);
        } else {
            System.out.println("没有节点了");
        }


    }
}

至此,循环链表操作完毕
时间复杂度(链表,以上三个基本都是这样) :
查找节点 : O(n)
插入节点:O(1)
更新节点:O(1)
删除节点:O(1)
你可能会有疑问,为什么上面的插入,更新,删除等等,他们的方法,操作了循环,但是还是O(1)呢
解释如下:
/*
实际上时间复杂度是针对操作(即单个操作和多个的单个操作)来说的,这里了解了,那么你就知道为什么了
为了帮助你了解,看如下解释:
这里我们回答之前在数组操作中,读取为什么是O(1)的原因,这是因为在获取数组中对应下标的元素时,我们是直接的通过下标获取,你可能会这样的认为,明明他查询所有时,是操作了遍历啊,为什么读取还是O(1)呢
如果是这样的认为,那么就错误了,因为你查询所有,实际上是多个操作结合的,如多个获取下标的操作等等,然后将所有获取的数组数据结合形成的,即如果你将该方法看成一个操作,那么自然是O(n),但是我们可以发现,他里面在循环中,都是通过下标去数组获取数据的,所以我们单纯的看这一步,很明显,自然是O(1),所以说实际上数组中,读取数据就是O(1)(之所以这样确定,看后面解释就知道了),我们只不过执行了多次这样的O(1),即本质上,他的读取就是O(1),即是单个操作,而这个方法是多个的单个操作
那么很明显,我们之前说的数组中的读取,是单个操作的读取,所以是O(1),当然,如果那里的读取有说明是方法的,那么自然就是O(n),但他那里也并没有说明,那么我们为什么也认为他不是方法呢,实际上在操作时,我们可以认定,将能满足的最小时间复杂度的操作来作为其对应操作的时间复杂度(我们一般不会这样认为),比如满足读取,那么数组能够满足读取的最小的时间复杂度自然就是直接的读取,即就是O(1)了,所以数组的读取就是O(1),而更新也可以直接的更新,所以也是O(1)
那么为什么数组的插入和删除是O(n)呢,还是按照上面的理论,我们在满足插入和删除时,必须要移动数据,且移动数据是因为他们的操作造成的,所以他们是O(n)
这里还要提一下,我们通常是不会认为将满足最小时间复杂度的操作来作为其对应操作的时间复杂度的,因为某些情况是特殊的,比如下面说明的尾部插入,但插入还是O(n)而不是O(1),所以在大多数情况下,我们是看操作本身的,即看他的操作是否因为他们自身造成的,然后分析他造成的操作的时间复杂度来估算他的总体时间复杂度
比如,上面的插入,我们在插入数据时,他插入的动作,必然要导致移动,所以是O(n),可能你也有疑问,如果先移动然后插入不行吗,实际上这样就是更新数据了,而不说插入这个操作,所以很明显,插入这个动作是一系列操作的结合,而不说单纯的赋值更新,所以插入是会导致其他操作的,否则就不是插入了,而是更新了
而读取,我们在读取数据时,基本上,就是直接的通过下标读取,而不会导致其他操作,就算是其他方法操作读取时,也是通过下标的,所以是O(1)
注意:他们的时间复杂度是整体的说明的(以后都是如此),而不是投机取巧说明的,比如说,我知道最后一个下标了,我们直接赋值来插入,我们也能认为是O(1),这虽然也能认为插入(在不考虑扩容的情况下,实际上是更新,但在某些插入操作中,或者就是插入,也称为尾部插入,如前面的说明,所以更新和尾部插入都可以表示),但是在整体上来说,他是忽略的,因为他只是特例,在数据无限大时,他就可以忽略了,因为首部插入和尾部插入都只是一次操作,在无限大的数据中,他们的2次是微不足道的,所以我们通常按照整体来判断,所以认为插入是O(n)
实际上在一些查找中(不是O(1)的)也类似,因为查找实际上有运气成分,所以我们的查找也按照最多的情况考虑时间复杂度(比如后面说明的链表的查找节点)


所以我们继续分析链表(看操作本身,且整体说明,因为头部插入是O(1),但整体方法来说,是O(n),虽然这里的插入节点是单纯操作的意思,即O(1)),时间复杂度基本都是这样)
查找节点:我们在找某个节点时,很明显,查找节点这个操作必然会操作遍历,即我们只能通过遍历来获取,所以时间复杂度就是O(n)
插入节点,删除节点,修改节点:
插入节点,删除节点,修改节点,这些动作只需要修改值即可,则基本认为是O(1),以插入为主,你可能认为插入明明已经会造成遍历了,但是,实际上我们可以只认为遍历,而插入只是随手操作的而已,即遍历那些操作并不是插入节点的操作的,而是查找节点的操作,因为查找节点是不属于插入操作的,即对于插入操作本身来说,他并不会造成其他操作,他只是通过修改指向而已,所以以此类推,插入节点,删除节点,修改节点都是O(1),这里可能又会提到数组的插入,前面说明过了,先移动的是更新,所以数组的插入还是O(n),因为很明显,他的插入的意思是移动插入,所以是O(n)
主要是由于"插入节点"不等于"插入",他们是不同的意思,这里要注意,所以不要以为他们是一样的,就认为时间复杂度一样
所以在分析之前,我们通常要明白我们对应的操作是什么意思(意思要弄清楚,否则怎么分析呢),然后再来根据是否引起其他操作(操作自然是我们加上的代码,要不然怎么能叫做操作呢,因为单纯一个类可是什么操作都没有的)来判断时间复杂度,后续我基本会说明对应的意思的

所以说在某种意思的情况下,时间复杂度是不同的,比如,我们虽然认为数组的插入会导致移动,所以是O(n),我们也的确这样认为的,这是因为该插入的意思是移动插入,但是,如果我们认为他的插入就是单纯的赋值,那么他的时间复杂度我们也认为是O(1),所以需要根据意思了
又比如,如果插入节点是单纯的插入,那么就是O(1),如果是结合的整体方法的插入,那么就是O(n)
很明显,不同的意思他们的时间复杂度是不同的,只是在数据结构中,对应的结构都默认是其固定的意思了,所以在有些博客中可以很明显的知道,他们基本都会认为链表插入是O(1),数组插入是O(n)
因为默认链表插入的意思的单纯的插入
而数组插入的意思是移动插入

但是在工作中,我们通常认为是方法
这里我们操作默认意思

所以无论链表是插入是O(n),还是O(1),都是根据其意思的
*/
我们可以知道,对时间复杂度的解释是复杂的,我们只需要记住"我们说明的操作,是否引起其他操作"来判断时间复杂度即可
如果对应的操作只需要一行代码,比如直接赋值(如修改节点),那么自然就是O(1)了
链表:优缺点
优势:插入、删除、更新效率高,省空间(因为不用扩容,即省下了扩容多余的空间,当然,扩容一般都是一次性是扩大很多的,这是为了防止多次的扩容,所以相对于这样的整体来说,的确是省很多空间的)
劣势:查询效率较低,不能随机访问
应用 :
链表的应用也非常广泛,比如树、图、Redis的列表、LRU算法实现、消息队列等
数组与链表的对比 :
数据结构没有绝对的好与坏,数组和链表各有千秋,就如算法一样,也是只有好和坏,因为他们都代表程序的一部分,他们都有同样的好与坏,在生活中我们也经常遇到这种,比如电脑的硬件配置,可能各有对应的优点

在这里插入图片描述

那么有个疑问,我们之前说过,时间复杂度是估算的,那么我们必须看操作的时间复杂度吗
实际上并不是,在某些时候,我们需要看他全部的操作,所以可能对应的具体操作的时间复杂度是小的
但是通常实际上操作时,可能是某些结合,所以时间复杂度低的实际上未必就好,比如链表的插入,必然需要查找
所以在某些情况下,我们也会认为链表的插入是O(n),这种情况则是根据具体方法来描述的
所以可能有些博客,说链表的插入操作是O(n)的时间复杂度(以后我们就用类似的O(n)来表示时间复杂度了,这5个字就不写了),也是有一定的道理的,但不是具体操作,而是该整体方法,才能这样说明
但是我们可以知道,链表的插入,首先会根据指针一路找过去的,然后进行指针赋值
而数组的插入先进行移动,然后赋值,我们可以发现,他们基本类似,但是数组的长度需要考虑(前面的内存存储中可以很明显的知道该链表在内存上比数组要好,也有具体的描述:“这样可以灵活有效地利用零散的碎片空间,这也是不使用数组,而使用链表的好处之一”)
所以在某些方面来说,链表在插入方面比较友好(省空间,不用扩容),但是可能在某些方面并不好
因为我们知道他只能从头节点开始,也就是说,他的优化的上限太低了
而数组,在某些时候,可以更好的利用某些算法来进行优化,比如我们直接的将数组进行二分(是分成两个数组,即基本不是后面的二分法),然后用两个线程来查找,即二分多线程(只针对数据的查找来进行优化,而不针对资源,这里这里是不考虑资源的,那么的确是优化了,如果考虑资源,那么多出一个线程虽然查找是快了,但是也消耗了更多的资源,所以在这种情况下不见得是优化的)
而链表因为链表地址的原因,所以基本很难这样使用线程操作,因为地址只能是一个,所以通常只能一步一步的操作,这也是之前我说的
为什么在下游中,数据结构的优化是有限的原因(不同的数据结构的上限不同,数组的上限一般都很大,链表一般很小)
虽然他的优化上限低,但是由于链表省空间,所以在下游中,他的使用还是很多的,上游有时也会使用
简单来说,在不考虑其他情况下(如优化),他们的优势如下:
数组的优势在于能够快速定位元素,对于读操作多、写操作少的场景来说,用数组更合适一些
链表的优势在于能够灵活地进行插入和删除操作而不用考虑内存空间的问题(数组需要扩容)
如果需要在尾部频繁插入、删除元素,用链表更合适一些(因为数组在尾部插入通常需要扩容)
数组和链表是线性数据存储的物理存储结构:即顺序存储和链式存储(前面已经说明了,这也是他们各自优势的本质)
从而数组可以直接用下标获取数据,而链表不要考虑内存存储空间
栈:
栈和队列都属于线性数据的逻辑存储结构(逻辑存储结构:我们认为他要怎么存放,就怎么存放,比如我们把大的放在前面,小的放在后面,这样也算逻辑存储结构,只是他们分线性和非线性的存储,这个从大到小,算是线性,也就是我们说的线性表和非线性表,前面有说明,如数组和链表,那里说明的就是逻辑存储结构,而不说物理存储结构,虽然他们本身就是物理结构的方式,但以逻辑结构而言,他们都是线性结构的)
由于他们是逻辑存储结构,所以通常需要物理结构(顺序存储结构和链式存储结构)来实现他们的具体效果,这里需要注意
实际上无论是线性结构还是非线性结构都需要物理结构来操作,比如数组和链表(虽然他们在逻辑上也称为线性结构)
即逻辑结构是:看起来是线性还是非线性
物理结构是:线性结构和非线性结构的实现方式
比如:数组看起来是线性结构,且使用他的物理结构方式是数组(顺序存储结构,而不是链式存储结构)
上述我们只是给数据结构进行某种定义,大多数情况下,这些只是专业术语,用来表示他们的形状的,我们并不需要过于理会

在这里插入图片描述

概念:
栈(stack)是一种线性数据结构,栈中的元素只能先入后出(First In Last Out,简称FILO)
最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶 (top)

在这里插入图片描述

存储原理:
栈既可以用数组来实现,也可以用链表来实现
栈的数组实现如下:

在这里插入图片描述

数组实现的栈也叫顺序栈(或静态栈)
栈的链表实现如下:

在这里插入图片描述

链表实现的栈也叫做链式栈(或动态栈,之所以不是静态栈,是因为不用扩容):
操作:
入栈(压栈):
入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶

在这里插入图片描述

在这里插入图片描述

出栈(弹栈):
出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会成为新的栈顶

在这里插入图片描述

接下来给出数组方式的栈实现的全部基本操作代码(记得自己写一下,然后再看哦):
/**
 *
 */
public class test3 {

    //定义一个数组
    int[] array;
    //定义一个栈指向位置,为什么要定义呢,因为我们要确定我们存放数的位置,而这个位置,单纯的使用数组是很难得到的
    //特别的是,如果我们入栈的是0,那么数组可能不好确定是否是他自身默认的,还是我们添加的,所以需要一个具体位置
    int count;
    //最大栈个数
    int max = 5;

    //定义一个指定空间大小的数组
    public test3(int n) {
        array = new int[n];
        //确定从0开始添加,这里可以不用加上,使用默认也行
        count = 0;
    }

    //入栈
    public void push(int i) {

        if (count + 1 <= max) {
            array[count] = i;
            count++;
            //这个array.length最好改成比自身的长度小的数,就算是array.length-1也行,即还是不要压的太紧了,特别的是在某些情况下我们需要保留一点空间
            //如在项目中,高并发的情况下要保留一定空间来进行扩容,这样能有效的防止突发情况的出现(如数组越界),在HashMap中也是这样的操作,这里就不改变了
            if (count >= array.length) {
                resize();
            }
            return;
        }
        System.out.println("已经到最大个数了,不能再入栈了");


    }

    //将数组进行扩容两倍
    public void resize() {
        int[] numsNew = new int[array.length * 2];
        System.arraycopy(array, 0, numsNew, 0, array.length);
        array = numsNew;
    }

    //出栈
    public void pop() {

        if (count == 0) {
            System.out.println("没有数据出栈了");
            return;
        }
        array[--count] = 0;

    }

    //打印数组中的所有数据
    public void select() {
        String a = "[";
        for (int j = 0; j < array.length; j++) {
            if (j == array.length - 1) {
                a = a + array[j] + "]";
            } else {
                a = a + array[j] + ",";
            }
        }

        System.out.println(a);
    }

    //反过来打印数组中的所有数据
    public void reverseSelect() {
        String a = "[";
        for (int j = array.length - 1; j >= 0; j--) {
            if (j == 0) {
                a = a + array[j] + "]";
            } else {
                a = a + array[j] + ",";
            }
        }
        System.out.println(a);
    }

    //查询最后一个入栈的数
    public void selectLast() {
        if (count != 0) {
            System.out.println("最后一个入栈的数是:" + array[count - 1]);
            return;
        }
        System.out.println("没有数据了");
    }

    //通过测试,发现基本没有什么问题(如果有某些问题,你可以试着修改一下),至此数组方式的栈操作就完成了
    public static void main(String[] args) {
        test3 t = new test3(2);
        t.select();
        t.reverseSelect();
        t.push(2);
        t.push(3);
        t.push(3);
        t.push(3);
        t.push(3);
        t.push(3);
        System.out.println("栈总数:" + t.count);
        t.select();
        t.reverseSelect();
        //最后一个入栈的数
        t.selectLast();
        t.pop();
        t.pop();
        System.out.println(1);
        t.select();
        t.reverseSelect();
        t.pop();
        t.select();
        t.reverseSelect();
        t.pop();
        t.select();
        t.reverseSelect();

    }
}

至此数组方式的栈的操作就完成了,接下来就是链表方式的栈操作(记得自己写一下,然后再看哦):
/**
 *
 */
public class test4 {

    Node head;
    int count;
    int max = 5;
    Node last;

    public class Node {
        int id;
        String name;
        Node next;

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

    //入栈
    public void push(Node node) {

        if (count + 1 <= max) {
            Node temp1 = head;
            Node temp2 = head;
            if (temp1 != null) {
                while (true) {
                    temp2 = temp1;
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        temp2.next = node;
                        last = node;
                        count++;
                        return;
                    }
                }
            }
            head = node;
            last = node;
            count++;
            return;

        }
        System.out.println("已经到最大个数了,不能再入栈了");

    }

    //出栈
    public void pop() {
        Node temp1 = head;
        Node temp2 = head;
        if (temp1 != null) {
            while (true) {
                if (temp1.next == null) {
                    head = null;
                    last = null;
                    count--;
                    return;
                }
                if (temp1.next != null) {

                    temp2 = temp1;
                    temp1 = temp1.next;
                    if (temp1.next == null) {
                        temp2.next = null;
                        last = temp2;
                        count--;
                        return;
                    }
                }


            }
        }
        System.out.println("没有节点出栈了");

    }

    //查询最后一个入栈的节点信息
    public void selectLast() {
        Node temp2 = head;
        Node temp1 = head;
        if (temp1 != null) {
            while (true) {
                temp2 = temp1;
                temp1 = temp1.next;
                if (temp1 == null) {
                    System.out.println("最后一个入栈的节点信息是:[" + temp2.id + "," + temp2.name + "]");
                    return;
                }
            }
        }
        System.out.println("没有入栈的节点信息了");


    }

    //查询所有节点
    public void select() {
        String a = "";
        Node temp1 = head;
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + "[" + temp1.id + "," + temp1.name + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp1.id + "," + temp1.name + "],";

            while (true) {

                temp1 = temp1.next;
                if (temp1.next == null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "]";
                    break; //这里就加上break,因为我们需要打印
                }
                if (temp1.next != null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "],";
                }


            }
        }
        if (temp1 == null) {
            a = "[]";
        }


        System.out.println(a);
    }

    //反过来查询所有节点,由于不是双向链表,所以这里需要进行特殊改动,这里我们的改动是,定义两个字符串来解决
    public void reverseSelect() {
        String a = "";
        String b = "";
        Node temp1 = head;
        String cc = "";
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + temp1.id;
                b = b + temp1.name;
                System.out.println("[" + a + "," + b + "]");
                return;
            }
            a = a + temp1.id + ",";
            b = b + temp1.name + ",";
            while (true) {


                temp1 = temp1.next;

                if (temp1.next == null) {
                    a = a + temp1.id;
                    b = b + temp1.name;
                    break;
                }
                if (temp1.next != null) {
                    a = a + temp1.id + ",";
                    b = b + temp1.name + ",";
                }


            }
            String[] aa = a.split(",");
            String[] bb = b.split(",");
            for (int j = aa.length - 1; j >= 0; j--) {
                if (j == 0) {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "]";
                } else {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "],";
                }

            }
        }
        if (temp1 == null) {
            cc = "[]";
        }


        System.out.println(cc);
    }

    //操作自己定义的变量来获得最后一个入栈的节点信息
    public void last() {
        if (last != null) {
            System.out.println("最后一个入栈的节点信息是:[" + last.id + "," + last.name + "]");
            return;
        }
        System.out.println("没有节点信息了");
    }


    //栈是不用考虑重复问题的和大小问题的以及其他多余问题,具体看前面说明的单链表代码,所以这里没有什么覆盖的操作或者大小判断以及其他操作,节省了很多代码,我们只需要完成他自身栈的作用即可,队列也是如此,所以以后我们只需要完成对应数据结构的大致操作,以后就不在说明了(也可以适当的添加一些操作,看心情吧),我们操作栈时,通过有个变量来记录对应要入栈的下标
    //他在数组中,可以表示操作总栈数,以及对应最后一个入栈的数,在链表中,通常只是记录总栈数的
    //最后一个入栈节点,通常是自己通过方法操作获取,因为这里没有下标,除非你自己定义一个变量来表示最后一个入栈的节点(上面有操作该变量的方法)
    //我们只需要输出这个变量即可,在数组操作和这里都是count变量,我们输出即可

    //通过测试,发现基本没有什么问题(如果有某些问题,你可以试着修改一下),至此链表方式的栈操作就完成了
    public static void main(String[] args) {
        test4 t = new test4();
        t.push(t.new Node(1, "2"));
        t.push(t.new Node(2, "2"));
        t.push(t.new Node(4, "2"));
        t.push(t.new Node(4, "2"));
        t.push(t.new Node(4, "2"));
        t.push(t.new Node(4, "2"));
        System.out.println("栈总数:" + t.count);
        t.select();
        //最后一个入栈的节点信息
        t.selectLast();
        t.reverseSelect();
        t.pop();
        t.pop();
        System.out.println("栈总数:" + t.count);
        t.select();
        t.selectLast();
        t.reverseSelect();
        t.pop();
        t.pop();
        t.select();
        t.selectLast();
        t.reverseSelect();
        System.out.println(66);
        t.last();


    }

}

//上面是将结尾作为入口和出口,这种方式不建议使用,因为他的入栈和出栈的方法(是方法而不是单纯的赋值)是O(n)
//实际上可以这样,我们将头部作为出栈方式,这样我们可以去除循环,这时就算是方法也能认为是O(1)
//因为我们的变量始终保存头部的,具体代码如下:

/**
 *
 */
public class test44 {
    Node head;
    int count;
    int max = 5;

    public class Node {
        int id;
        String name;
        Node next;

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

    //入栈
    public void push(Node node) {
        if (count + 1 <= max) {
            node.next = head;
            head = node;
            count++;
            return;
        }
        System.out.println("已经到最大个数了,不能再入栈了");
    }

    //出栈
    public void pop() {
        if (head != null) {
            if (head.next == null) {
                head = null;
                count--;
                return;
            }
            head = head.next;
            count--;
            return;
        }
        System.out.println("没有节点出栈了");
    }

    //查询所有节点
    public void select() {
        String a = "";
        Node temp1 = head;
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + "[" + temp1.id + "," + temp1.name + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp1.id + "," + temp1.name + "],";

            while (true) {

                temp1 = temp1.next;
                if (temp1.next == null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "]";
                    break; //这里就加上break,因为我们需要打印
                }
                if (temp1.next != null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "],";
                }


            }
        }
        if (temp1 == null) {
            a = "[]";
        }


        System.out.println(a);
    }

    //反过来查询所有节点,由于不是双向链表,所以这里需要进行特殊改动,这里我们的改动是,定义两个字符串来解决
    public void reverseSelect() {
        String a = "";
        String b = "";
        Node temp1 = head;
        String cc = "";
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + temp1.id;
                b = b + temp1.name;
                System.out.println("[" + a + "," + b + "]");
                return;
            }
            a = a + temp1.id + ",";
            b = b + temp1.name + ",";
            while (true) {


                temp1 = temp1.next;

                if (temp1.next == null) {
                    a = a + temp1.id;
                    b = b + temp1.name;
                    break;
                }
                if (temp1.next != null) {
                    a = a + temp1.id + ",";
                    b = b + temp1.name + ",";
                }


            }
            String[] aa = a.split(",");
            String[] bb = b.split(",");
            for (int j = aa.length - 1; j >= 0; j--) {
                if (j == 0) {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "]";
                } else {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "],";
                }

            }
        }
        if (temp1 == null) {
            cc = "[]";
        }


        System.out.println(cc);
    }

    public static void main(String[] args) {

        test44 t = new test44();
        t.push(t.new Node(1, "2"));
        t.push(t.new Node(1, "2"));
        t.push(t.new Node(2, "2"));
        t.push(t.new Node(2, "2"));
        t.push(t.new Node(2, "2"));
        t.push(t.new Node(25, "2"));
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.select();
        t.reverseSelect();
        //最后一个入栈的就是head
        if (t.head != null) {
            System.out.println("最后一个入栈的节点信息是:[" + t.head.id + "," + t.head.name + "]");

        } else {
            System.out.println("没有节点信息了");
        }
    }
}


//我们可以很明显的发现,以头部作为入栈的出口和入口,比以尾部作为入栈的出口和入口要简单许多
//我们也能更加的证明,算法的确是有好有坏的,很明显,以结尾作为出口和入口就是坏的算法,而已头部作为出口和入口就是好的算法,虽然他们结合起来是一个数据结构,所以实际上数据结构真正的本质是赋值,而其他数据结构是本质数据结构和算法组成的,我们也将他们的组成或者说,现在都统一的认为也是数据结构了,这是因为经历过很多的迭代,他们的确是非常好的,除非你可以自己创建一个,只要你创建的有优势,那么你创建的也可以认为是数据结构,只要被认可即可
至此链表方式的栈的操作就完成了
注意:这个栈的数组方式和链表方式,都是结尾当成顶部的,当然你也可以将头部当成顶部
但无论是哪个方向,只要是一方进入或者出去即可,这样的都可以称为栈,实际上链表操作结尾为顶部比操作头部为顶部要困难一点,但上面是解决了的(挑战难度哦)
这里我们可以发现,他们都用count变量来操作,且操作了最大个数的判断
所以我们可以针对该count变量来确定栈最大能保存多少个数,当然,我们最好是设置该变量的,防止过分的无上限的使用内存
但是在大多数的情况,我们都会放开限制(甚至不会限制,即去掉判断),只有某些需要空间的情况我们是不会放开限制的,具体看当时的业务的
通常是先放开限制,然后如果因为他的数量实在太多,导致出现了某些内存问题,可以适当的减少最大数
时间复杂度:
入栈和出栈的时间复杂度都是O(1),他们的意思是针对与他们自身的操作(单纯的赋值),所以是O(1)
支持动态扩容的顺序栈:操作了扩容
当数组空间不够时,我们就重新申请一块更大的内存,将原来数组中数据统统拷贝过去
这样就实现了 一个支持动态扩容的数组,通过前面的操作代码,所以当入栈在最后时(数组和数组方式的入栈好像都是在添加最后一个下标值之后进行扩容的),那么这个入栈通常就会导致扩容
所以可以得知入栈的意思有两种
第一种:普通的入栈,那么是O(1)
第二种:扩容入栈,那么就是O(n)
所以这里是特别的,那么可以得出,如果数组的插入也是扩容插入(前面说明时间复杂度解释里面的尾部插入是认为不考虑扩容的,所以是O(1)),那么他也是O(n),因为他这个插入,必须要扩容,或者说,要进行扩容,就如我们普通插入需要移动一样,都是我们添加的操作
虽然他本身也是O(n),只是他们是不同的O(n),这只是估算的
那么可以再次的得出,就算是相同的估算,即相同的时间复杂度,可能他们的效率也是不同
只是都是相同的提升而已,即根据n提升,就如2n和9n,虽然我们说,当n无限大时,系数可以忽略,但是通常他们还是有差别的,因为我们的计算机不可能出现无限大的n
应用:
函数调用:每进入一个函数,就会将临时变量作为一个栈入栈(main方法的独有栈里面),然后他自身这个方法也会开辟栈来存放他自身的操作
当被调用函数执行完成,返回之后,才会将这个函数对应的栈帧出栈(不是单纯的出栈的意思),即删除该整个栈,而不是出栈main方法(前提是他里面的操作执行完毕,底层有联系的,虽然看不到),具体作用实现,在后面的递归中会进一步说明
所以每个函数基本有独有的栈来完成他自己的操作,这是基本的内存结构的应用
可以百度搜索栈(也称为栈区),堆(也称为堆区),方法区(好像都不会称为方法,所以基本只称为方法区),就知道了,这里主要说明的是栈
浏览器的后退功能:
我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈X 中出栈
并将出栈的数据依次放入栈 Y,当我们点击前进按钮时,我们依次从栈 Y 中取出数据, 放入栈 X 中
当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了(通常刚进入浏览器,浏览器都会给一个默认页面给x栈,这就是为什么有些浏览器进入后,会出现一个页面的原因,当我们关闭所有页面时,会释放x栈和y栈,然后浏览器退出,一般是先关闭窗口,然后释放x栈和y栈,最后关闭浏览器,这是基本是一瞬间的事情,所以是很快的哦)
当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了(一般来说,我们自己加页面时,通常会清空y栈,所以会发现,自己加上页面时,基本不能前进了,只能先后退,然后才可以前进)
但也要注意:x栈和y栈只是针对一个窗口,所以不同窗口是不同的x栈和y栈的,他们直接互不影响
这就是为什么浏览器操作可以前进和后退的一个原因,我们每次的浏览一个新的页面都会放在x栈中
很明显,最后入栈的就是我们浏览的页面,即x栈(顶部为主)是主要浏览,而y栈(顶部为主)保存浏览
队列:
概念:
队列(queue)是一种线性数据结构,队列中的元素只能先入先出(First In First Out,简称 FIFO)
队列的出口端叫作队头(front),队列的入口端叫作队尾(rear)

在这里插入图片描述

存储原理:
队列这种数据结构既可以用数组来实现,也可以用链表来实现
数组实现:

在这里插入图片描述

用数组实现时,为了入队操作的方便,把队尾位置规定为最后入队元素的下一个位置
用数组实现的队列叫作顺序队列
链表实现:

在这里插入图片描述

用链表实现的队列叫作链式队列
操作:
入队:
入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置放入元素,新元素的下一个位 置将会成为新的队尾

在这里插入图片描述

出队:
出队操作(dequeue)就是把元素移出队列,只允许在队头一侧移出元素,出队元素的后一个元素将会成为新的队头

在这里插入图片描述

接下来给出数组方式的队列实现的全部基本操作代码(记得自己写一下,然后再看哦):
/**
 *
 */
public class test5 {

    int[] nums;

    //由于队列有两个出口,所以我们需要用两个变量来表示入口和出口的位置
    int head;
    int last;

    //在队列中,我们通常都会进行限制,而不会放开限制,比其他数据结构要限制一点
    int max = 5;

    //自己定义一个数组内存空间大小
    public test5(int n) {
        nums = new int[n];

        //注意:他们是表示数的入口和出口的,而不是数组的尾部和头部,所以要注意
        head = 0; //出列的部分
        last = 0; //入列的部分
        //我们一般都用头部表示出列,而不会用尾部表示出列,当然你反过来也可,主要看你的意愿了
        //这里以头部表示出列,尾部表示入列,后面的链表方式操作队列也是如此
    }

    //入列
    public void push(int a) {
        if (last - head + 1 <= max) {
            nums[last] = a;
            last++;
            if (last >= nums.length) {
                resize();
            }
            return;
        }
        System.out.println("已经到最大个数了,不能再入列了");

    }

    //出列
    public void pop() {
        if (head != last) {
            nums[head] = 0;
            head++;
            return;
        }
        System.out.println("没有数据出列了");
    }

    //将数组进行扩容两倍
    public void resize() {
        int[] numsNew = new int[nums.length * 2];
        System.arraycopy(nums, 0, numsNew, 0, nums.length);
        nums = numsNew;
    }

    //通过上面我们可以发现,他们并没有进行移动数据的操作,所以我们可以编写一个方法来完成这样的操作,并重置head和last的变量位置
    public void reset() {
        int[] array = new int[nums.length];
        int j = 0;
        for (int i = head; i < last; i++, j++) {
            array[j] = nums[i];
        }
        nums = array;
        head = 0;
        last = j;

    }

    //打印数组中的所有数据
    public void select() {

        String a = "[";
        for (int j = 0; j < nums.length; j++) {
            if (j == nums.length - 1) {
                a = a + nums[j] + "]";
            } else {
                a = a + nums[j] + ",";
            }
        }

        System.out.println(a);
    }

    //反过来打印数组中的所有数据
    public void reverseSelect() {
        String a = "[";
        for (int j = nums.length - 1; j >= 0; j--) {
            if (j == 0) {
                a = a + nums[j] + "]";
            } else {
                a = a + nums[j] + ",";
            }
        }
        System.out.println(a);
    }

    //查询准备出列的数和最后一个入列的数
    public void selectHeadLast() {
        if (head != last) {
            System.out.println("准备出列的数是:" + nums[head]);
            System.out.println("最后一个入列的数是:" + +nums[last - 1]);
            return;
        }
        System.out.println("没有出列的数据了");
    }

    public static void main(String[] args) {
        test5 t = new test5(2);
        t.push(2);
        t.push(2);
        t.push(3);
        t.push(3);
        t.push(3);
        t.push(3);
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.select();
        t.reverseSelect();
        t.reset();
        t.select();
        System.out.println(t.head);
        System.out.println(t.last);
        //准备出列的数和最后一个入列的数
        t.reverseSelect();
        t.selectHeadLast();

    }

}

至此数组方式的队列的操作就完成了,接下来就是链表方式的队列操作(记得自己写一下,然后再看哦):
/**
 *
 */
public class test6 {
    Node head;
    //这两个变量自然是有用的,比如可以操作节点数,虽然链表对应的变量(使用链表操作数据结构一般需要的变量)没有数组对应的作用大,但是还是有作用的
    //主要是数组可以直接使用下标,而链表不能,所以导致链表的对应变量没有数组的有作用
    int countHead;
    int countLast;
    //很明显这里代表他们直接的入列和出列的次数,一般用来操作max变量

    int max = 5;

    //最后一个入列的节点,准备出列的节点就是head,所以不用定义变量了
    Node last;


    public class Node {
        int id;
        String name;
        Node next;

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

    //入列
    public void push(Node node) {
        if (countLast - countHead + 1 <= max) {
            Node temp1 = head;
            Node temp2 = head;
            if (temp1 != null) {
                while (true) {
                    temp2 = temp1;
                    temp1 = temp1.next;
                    if (temp1 == null) {
                        temp2.next = node;
                        countLast++;
                        last = node;
                        return;
                    }
                }
            }
            head = node;
            last = node;
            countLast++;
            return;
        }
        System.out.println("已经到最大个数了,不能再入列了");
    }

    //出列
    public void pop() {
        if (head != null) {
            head = head.next;
            countHead++;
            return;
        }
        System.out.println("没有数据出列了");
    }

    //查询所有节点
    public void select() {
        String a = "";
        Node temp1 = head;
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + "[" + temp1.id + "," + temp1.name + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp1.id + "," + temp1.name + "],";

            while (true) {

                temp1 = temp1.next;
                if (temp1.next == null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "]";
                    break; //这里就加上break,因为我们需要打印
                }
                if (temp1.next != null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "],";
                }


            }
        }
        if (temp1 == null) {
            a = "[]";
        }


        System.out.println(a);
    }

    //反过来查询所有节点,由于不是双向链表,所以这里需要进行特殊改动,这里我们的改动是,定义两个字符串来解决
    public void reverseSelect() {
        String a = "";
        String b = "";
        Node temp1 = head;
        String cc = "";
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + temp1.id;
                b = b + temp1.name;
                System.out.println("[" + a + "," + b + "]");
                return;
            }
            a = a + temp1.id + ",";
            b = b + temp1.name + ",";
            while (true) {


                temp1 = temp1.next;

                if (temp1.next == null) {
                    a = a + temp1.id;
                    b = b + temp1.name;
                    break;
                }
                if (temp1.next != null) {
                    a = a + temp1.id + ",";
                    b = b + temp1.name + ",";
                }


            }
            String[] aa = a.split(",");
            String[] bb = b.split(",");
            for (int j = aa.length - 1; j >= 0; j--) {
                if (j == 0) {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "]";
                } else {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "],";
                }

            }
        }
        if (temp1 == null) {
            cc = "[]";
        }


        System.out.println(cc);
    }

    //查询准备出列的节点信息和最后一个入列的节点信息
    public void selectHeadLast() {
        if (head != null) {
            System.out.println("准备出列的节点信息是:[" + head.id + "," + head.name + "]");
            System.out.println("最后一个入列的节点信息是:[" + last.id + "," + last.name + "]");
            return;
        }
        System.out.println("没有节点信息了");
    }

    public static void main(String[] args) {
        test6 t = new test6();
        t.push(t.new Node(14, "2"));
        t.push(t.new Node(1, "2"));
        t.push(t.new Node(12, "2"));
        t.push(t.new Node(121, "2"));
        t.push(t.new Node(121, "2"));
        t.push(t.new Node(121, "2"));
        t.select();
        t.reverseSelect();
        t.selectHeadLast();
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.pop();
        t.select();
        t.reverseSelect();
        t.selectHeadLast();

    }


}

//实际上我们可以定义两个节点变量来实现该链表方式的队列,我们只需要在这两个变量之间进行入列和出列
//如果使用这样的方式可以更加的简便,代码如下:
/**
 *
 */
public class test66 {
    Node head;
    Node last;
    int count;
    int max = 5;

    public class Node {
        int id;
        String name;
        Node next;

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

    //入列
    public void push(Node node) {
        if (count + 1 <= max) {
            if (head == null) {
                head = node;
                last = node;
                count++;
                return;
            }

            //这里需要理解一下,我们只是改变对象对应的值,我们的头部是始终获取的,也就是说,头部保证不变,尾部往后面添加
            last.next = node;
            last = node;
            count++;
            return;
        }
        System.out.println("已经到最大个数了,不能再入列了");
    }

    //出列
    public void pop() {
        if (count != 0) {
            //一般,原来的对象没有被指向,那么垃圾回收机制可能会将他回收,这里注意即可
            //那么会影响其他对象吗,答:不会,因为他只是回收保留的地址,而不会回收地址对应的对象
            //就如我们定义一个变量,他有值,如果他被回收,也只会回收他,而不会回收他的对象
            //除非他的对象也没有被人指定(没有其他变量指定)
            head = head.next;
            count--;
            return;
        }
        System.out.println("没有节点出列了");

    }

    //查询所有节点
    public void select() {
        String a = "";
        Node temp1 = head;
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + "[" + temp1.id + "," + temp1.name + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp1.id + "," + temp1.name + "],";

            while (true) {

                temp1 = temp1.next;
                if (temp1.next == null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "]";
                    break; //这里就加上break,因为我们需要打印
                }
                if (temp1.next != null) {
                    a = a + "[" + temp1.id + "," + temp1.name + "],";
                }


            }
        }
        if (temp1 == null) {
            a = "[]";
        }


        System.out.println(a);
    }

    //反过来查询所有节点,由于不是双向链表,所以这里需要进行特殊改动,这里我们的改动是,定义两个字符串来解决
    public void reverseSelect() {
        String a = "";
        String b = "";
        Node temp1 = head;
        String cc = "";
        if (temp1 != null) {
            if (temp1.next == null) {
                a = a + temp1.id;
                b = b + temp1.name;
                System.out.println("[" + a + "," + b + "]");
                return;
            }
            a = a + temp1.id + ",";
            b = b + temp1.name + ",";
            while (true) {


                temp1 = temp1.next;

                if (temp1.next == null) {
                    a = a + temp1.id;
                    b = b + temp1.name;
                    break;
                }
                if (temp1.next != null) {
                    a = a + temp1.id + ",";
                    b = b + temp1.name + ",";
                }


            }
            String[] aa = a.split(",");
            String[] bb = b.split(",");
            for (int j = aa.length - 1; j >= 0; j--) {
                if (j == 0) {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "]";
                } else {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "],";
                }

            }
        }
        if (temp1 == null) {
            cc = "[]";
        }


        System.out.println(cc);
    }

    public static void main(String[] args) {
        test66 t = new test66();
        t.push(t.new Node(1, "2"));
        t.push(t.new Node(1, "2"));
        t.push(t.new Node(1, "2"));
        t.push(t.new Node(2, "2"));
        t.push(t.new Node(1, "2"));
        t.push(t.new Node(12, "2"));
        t.select();
        t.reverseSelect();
        t.pop();
        t.pop();
        t.pop();
        if (t.head != null) {
            System.out.println("准备出列的节点信息是:[" + t.head.id + "," + t.head.name + "]");
            System.out.println("最后一个入列的节点信息是:[" + t.last.id + "," + t.last.name + "]");

        } else {
            System.out.println("没有节点信息了");
        }
        t.select();
        t.reverseSelect();
        t.pop();
        t.pop();
        t.pop();
        t.select();
        t.reverseSelect();

    }
}

//很明显,这种方式,极大的简便了原来的代码,且出列和入列无论是方法还是单纯的操作,都是O(1)了
//而原来的方式,他只是寻找最后节点,这是不好了,为什么不直接将一个变量始终指向最后一个节点呢,你说是吧,这里就是这种方式

//从这里可以看成,无论是栈的链表还是队列的链表,他们有不同的方式,而他们数组,确基本没有,因为他们数组的操作基本是没有循环的,而栈和链表的方式且有不同,但我们其实可以发现,他们新的方式,是使用类似于数组的思想,而不是链表的思想,即将链表看成数组来使用,即他们都是将一端(头部或者尾部或者相关的位置)用某个变量来始终指定,即该变量始终跟踪,该变量通常能完成需要的操作或者找到对应数据(比如数组的尾部,并不是指定尾数据,而链表是指定尾部数据的,但他们都可以操作或者找到对应数据),所以这里我们也称该算法为跟踪算法(一般对一端操作时使用,一端一般是指最开始或者最后,如头部和尾部,即前面的链表,如数组下标0或者数组最后一个下标,或者最前面的数或者下标或者最后面的数或者下标,即前面的数组,这里是这样表示的,在网上他也有其他表示,名称就是用来表示的,自己创建一个不过分吧),所以我们有时候对数组和链表的实现可以不同的(数组基本没有上面优化的地方,链表还有,如使用跟踪算法),在有些时候,我们不要只使用数据结构本身的操作,比如数组,我们可以不操作数组本身的结构来循环查询数据,可以直接使用二分(基本不是后面的二分法)多线程来查询,而链表,我们也可以不使用他本身插入尾部节点的方式,可以直接使用跟踪算法来进行插入

//这也是数组和链表的其他扩展实现方式,当然可能还有其他扩展,而这些扩展,我们在操作或者编写其他需要使用他们的数据结构时,可以使用到,就如前面的单纯的使用链表和使用跟踪算法这两种不同的操作,所以对物理结构的运用,在很大程度上可以优化很多使用他们的逻辑结构
//而正是使用了跟踪算法,所以实际上链表在某些时候,他的尾部插入可以是O(1)(对方法来说),而不是O(n)(对方法来说)
//特别的,如果是单链表,我们可以使用始终的尾部插入来实现,所以可以实现单链表的添加的O(1)


//那么有个问题,这个算法是怎么想到的呢,这就要考虑,对本质数据结构(赋值)和自己的逻辑思维了,就如写数学题一样,你可能会尝试很多种办法来解决这个问题,但是你的这些办法中,总会有一个好的,那么过了许久,你可能突然开窍了,找到了非常简便的方法(如数学的公式的变化),这就像上面的链表的跟踪算法一样,即就是找到的一个非常简便的方法,可能他这个算法并不能很简便的减少他链表自身的不好之处(公式就是公式,基本上他很难再次的优化了,但他的变形可能会有更快的做出题来,即有好处),但是可能会优化使用他的一些逻辑结构(更快的做出题来),比如上面的队列,就如你在数学上的小突破,可能会突然就知道能简便的解决某些问题了,如公式的变化等等

//我们可以认为,一个1+1可以变成很多方式,比如2sinx^2+2conx^2=?,很明显,这个?代表链表
//而如果解决2sinx^2+2conx^2的结果代表算法,我们可以通过公式变化,即sinx^2+conx^2=1来更加的简便操作,且他也在其他地方有能够更简便的解决某些问问题,但解决方式有多样,比如这里的公式,或者看图形来解决(需要图,即需要时间,可以认为是比较公式来说是坏的,只是针对编程来说,因为针对数学来说,有利于你的理解,而编程只是分性能好和坏,而不分你的理解好和坏,即针对一个,即出结果时间,省略不相关,使得可能省略好处,而不是对数学的多个,即比如出结果时间和理解,都考虑)
//即一个结果对应多种方式,所以说,算法基本是没有固定的思维的,主要看你如果实现,基本没有什么模板,就如做数学题一样,有多种方式,你想到什么就用即可,虽然有好有坏


//实际上上面说明的跟踪算法,只是帮我们保存了,要操作的位置,而免除循环了,很明显保存数据也是数据结构的一种使用,所以下游的确是这样的
至此链表方式的队列的操作就完成了,至此线性表大致介绍完成
时间复杂度:
入队和出队都是O(1),他们的意思是针对与他们自身的操作(单纯的赋值,如果是后面的方式,那么就都是O(1)),所以是O(1)
应用:
资源池、消息队列、命令队列(有些事务也是使用队列的,比如redis事务,但是他是添加语句一起执行,而有些没有,比如mysql事务的语句可以认为是实时添加执行,只是认为而已,然后回滚,但我们也可以这样认为,在sql中,他操作了栈,只是他会有操作将语句进行反向操作存放队列,所以如果回滚的话,执行这些反向语句即可)等等
接下来我们来说明散列表(线性表大致说明完成了)
散列表 :
概念:
散列表也叫作哈希表(hash table),这种数据结构提供了键(Key)和值(Value)的映射关系
只要 给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)
我们可以认为(只是认为而已):key相当于数组的下标,value就是对应数组的值

在这里插入图片描述

存储原理:
哈希函数:
散列表在本质上也是一个数组,一般都是这样
散列表的Key则是以字符串类型为主的(注意:是为主,所以有时候基本上任何类型都可以,一般没有特别指明的,那么就是Object的类型,所以基本任何类型都可以,但是通常只能是引用类型,因为需要调用哈希函数,所以在操作key时,如果是赋值给key,那么基本类型通常会自动装箱,如果是直接调用,那么就需要手动装箱来调用哈希函数,String实际上并不能称为基本类型,只是使用的多了,我们也通常认为他是,但实际上是引用类型,所以说有时候基本类型会包括String,但他并不会是基本类型的统一描述范围,只是认为他在而已)
通过hash函数把Key向数组下标进行转换,因为通过上面的解释,我们可以知道,他们基本上可以是任何类型,那么为了统一划分,所以需要进行转换,这就是为什么需要hash函数的原因
所以hash函数的作用是把任意长度或者类型的输入,通过散列算法转换成固定类型、固定长度的散列值(哈希值)
实际上散列值(哈希值)通常是通过引用地址对应的对象的哈希函数的哈希值(通常都是重写哈希函数的,如果不重写,实际上只是默认Object的该方法,该方法基本不是java代码的,因为是native修饰的,所以通常不能调试,但调用是可以的,所以我们可以获得地址信息,而该方法就是地址的结果,而不同地址的结果,基本不会出现相同的,除非是相同的地址)来进行的,而由于是数组方式,可能容易会相同,以后说明的哈希值都是重写的哈希值了,就不说明不重写的了
虽然哈希方法可以重写,但有些情况下,你引用地址对应的对象的重写的哈希函数的方法可能会导致不同引用对象(引用地址对应的对象)出现相同的哈希值,即不同地址出现相同哈希值(哈希方法产生的值,无论是否重写都是说明该值,以后默认是重写哈希函数的哈希值),即到那个时候,可能不同的引用确出现了相同的下标(当然如果针对数组,那么下标越小越容易相同,因为他操作计算了,且是容易相同的取模%,但是仅仅只是针对自带的重写的哈希方法产生的哈希值来说,通常需要数据量大的情况下,才会出现相同结果,因为既然是重写的,自然会考虑很多情况,一般自带的很难出现相同结果,而我们手动写的可能容易出现,除非你考虑的更多,那么也是很难出现的,而对于没有重写的,不同引用地址的结果,基本不会出现相同的)
自带的:java自带的类(对象,引用)
而大多数的基本自带的引用基本都重写了,且是根据自身特点来重写,即对应引用的对象相同的结果(他们自身引用对象相同的值,而不是不同引用,因为他们通常是根据自身变量来重写的,比如只要你的某个变量相同,那么重写的哈希函数的结果就会相同等等操作,极少数出现不同变量值会相同的案例,也就是上面说的,不同引用对象可能会相同,因为你变量的值基本也是重写哈希函数的,简单来说,就是他们只操作自己变量的哈希值,而不是本身),通常哈希值相同,我们自己如果要写的话,最好也重写,否则就是地址了,那么基本不会相同
总结:不重写的哈希值(不重写的哈希函数的哈希值),不操作计算,基本不会相同,重写的哈希值操作计算可能会容易一点相同(自带的重写方法),或者操作取模(他通常是利用别人重写的哈希值,即重写哈希函数的哈希值来操作,比如我们手动或者某些自带的某些重写方法或者自带方法,如HashMap集合,他通常是自带方法,但他会有一个界限来保证不会全部看数组长度,比如一个数组长度为6,我添加了三分之一我就扩容,来使得不会容易相同),那么更加容易相同(即哈希冲突),无论是不同地址其哈希值相同也好还是不同也好,操作取模,结果都容易相同,只是相同哈希值的下标必然一样而已
我们通常也一般将操作或者说使用哈希值产生的值,也叫做哈希值,比如操作取模

在这里插入图片描述

而由于我们操作的散列表是以数组为主的,所以我们需要考虑数组方面的哈希冲突
以Java为例:
//一般我们将通过hashcode()这个的哈希函数生成的值,叫做哈希值,之所以叫哈希,那么肯定是有原因的:
//原因如下:
//哈希是Hash的音译,意为"散列",通常认为的意思是:混杂、一团糟

//比如我们可以这样,将取key的hashcode模(%)数组的长度后的余数作为数组下标,因为余数本身的就是0,也正好与数组下标对应
index = HashCode (Key) % Array.length 
    //如数组长度为10,那么结果只能是0到9,看如下:
int index=Math.abs("Hello".hashCode())%10; //是(0-9)之间的结果,这里代表是0,因为Hello的哈希值基本是69609650
    //static int abs(int a),返回参数指定数值的绝对值


//实际上哈希函数对输入的长度和类型都有不同的结果,特别是长度,越长,结果越大,类型的话,主要看类里面的内容了
//基本上很难确定数据会变的有多大,但基本上数据越多结果越大
//他们可能都会运用地址的信息(也有可能不使用,因为是重写的,按照他们自己的想法来,比如String类或者说String引用对象或者说String引用),来保证唯一,但是也正是因为对应的引用的对象大多数是重写哈希函数的,而基本不完全的是地址,那么可能他们的计算中,在某个数据量下,会相同,也就是哈希冲突(无论是针对数组下标还是哈希值本身,都可以认为是哈希冲突,即通过哈希值变化的,无论是他本身还是变化后的值,只要相同,都可以认为是哈希冲突),当然,通常是不会出现的,需要数据量大的情况下(针对哈希值本身),可能会出现,如果是数组,那么数组长度越小越容易出现哈希冲突,因为他操作计算了,且是容易相同的取模%
这基本是最简单的计算方式
还有很多hash函数:CRC16、CRC32、siphash 、murmurHash、times 33等
此种Hash计算方式为固定Hash方式,也称为传统Hash
该方式在数组固定时,可以快速检索
但当数组长度变化时,需要重新计算数组下标,否则根据key检索将出现问题
比如数组长度是3,变成了5,而你的数据哈希值是18(他是不变的,因为哈希只是确定数组下标,但是并不会改变数据,即他只是用来确定我们将该数据放在那个下标而已),很明显,最后的下标由0,变成了3
即如果不重新计算数组下标,使得数据移动,那么原来的数据还在0下标那里,但我们确读取了3这个下标值,即出现了问题
而且,这里也需要考虑3这个下标是否存在的问题,总不能重新hash后,即重新操作计算或者操作哈希值后,有多个下标3吧,比如还有个8,原来是2,现在也是3了,即是否发生覆盖的问题,所以通常我们会对该值采用链表(后面还有开放寻址法,虽然也行,但是链表方式比较好,会说明区别的)来操作,而HashMap也就是这样操作的
所以说传统Hash法虽然比较简单,但不利于扩展,如果要扩展可以采用一致性Hash法
操作:
写操作(put) 写操作就是在散列表中插入新的键值对(在JDK中叫作Entry或Node) ,很明显,这个数组是Entry数组或Node数组
第1步,通过哈希函数,把Key转化成数组下标
第2步,如果数组下标对应的位置没有元素,就把这个Entry填充到数组下标的位置

在这里插入图片描述

Hash冲突(碰撞):
由于数组的长度是有限的,当插入的Entry越来越多时,不同的Key通过哈希函数获得的下标有可能是相同的
这种情况,就叫作哈希冲突

在这里插入图片描述

解决哈希冲突的方法主要有两种:
开放寻址法:
开放寻址法的原理是当一个Key通过哈希函数获得对应的数组下标已被占用时,就寻找下一个空档 位置

在这里插入图片描述

在Java中,ThreadLocal所使用的就是开放寻址法,但是很明显,他存放时,需要往后遍历出null才可,虽然都是循环,但是对于读取来说,开发寻址法,通常需要判断对应的key计算哈希是否相同,虽然这不是重要的,最重要的是,当数组长度越来越长时,无论是添加还是读取,都比链表需要更多的时间(即相当于单纯的数组,而如果是单独的链表,而不是数组加链表,那么与单纯的数组基本类似,但基本上是不能的,因为没有相关哈希值的指定位置,即就是链表了,就算不操作哈希,但是由于覆盖的原因,也需要循环,即他相当于开放寻址了,因为要操作整个链表,虽然数组是指定的后面,即数组好点,即以数组为主,所以也只有单纯数组和数组加链表,对应与开放寻址法和链表法),所以这是主要的,因为你判断了无关的数组链表节点(链表类,不是节点类),而链表判断的基本都是有关的,所以时间复杂度虽然一样,但是链表的还是要少点的,因为规模n的提升是不相同的(因为特殊常数,前面说明过),且容易扩容,不易于利用空间(链表好利用,因为随机存储,而不是数组的顺序存储)
综上所述,我们大多数都是使用链表法:
链表法:
数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点
每一个Entry对象通过next指针指向它的下一个Entry节点
当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可,默认next指向null

在这里插入图片描述

在Entry中保存key和值,以及next指针,比如:
Entry{
 int key;
 Object value;
 Entry next;
}
当根据key查找值的时候,在index=2的位置是一个单链表,遍历该单链表,再根据key即可取值(value)
在java中的HashMap集合中,就是使用这种方式
读操作(get):
读操作就是通过给定的Key,在散列表中查找对应的Value
第1步,通过哈希函数,把Key转化成数组下标
第2步,找到数组下标所对应的元素,如果key不正确,说明产生了hash冲突, 则顺着头节点遍历该单链表,再根据key即可取值

在这里插入图片描述

Hash扩容(resize):
散列表是基于数组实现的,所以散列表通常需要扩容
当经过多次元素插入,散列表达到一定饱和度时,Key映射位置发生冲突的概率会逐渐提高
这样 一来,大量元素拥挤在相同的数组下标位置,形成很长的链表
对后续插入操作和查询操作的性能都有很大影响
影响扩容的因素有两个:
这里以HashMap为例:
Capacity:HashMap的当前长度
LoadFactor:HashMap的负载因子(阈值),默认值为0.75f
当HashMap.Size >= Capacity×LoadFactor时,需要进行扩容,即当数据(不是数组长度,而是我们保存了多少数据)的大小如果达到了数组长度的四分之三时,就进行扩容,在前面也举了个例子:比如一个数组长度为6,我添加了三分之一我就扩容,来使得不会容易相同
即不会容易发生冲突
扩容的步骤:
举个例子(还是以hashMap为例):
1:扩容,创建一个新的Entry空数组,长度是原数组的2倍
2:重新Hash,遍历原Entry数组(包括链表),把所有的Entry重新Hash到新数组中,有相同的,自然操作链表
然后我们指向这个新的即可,而没有指向的数组,自然会被垃圾回收机制给回收(大多数都会回收的)
除了极少数情况(比如太卡了,卡到垃圾回收机制操作缓慢,或者不操作了,虽然我没有见过,以后会说明的垃圾回收机制的),那么基本只能选择重启释放了

在这里插入图片描述

关于HashMap的实现,JDK 8及其之后的版本和他以前的版本有着很大的不同
当多个Entry被Hash到同一个数组下标位置时,为了提升插入和查找的效率
HashMap会把Entry的链表转化为红黑树这种数据结构
而JDK1.8(也叫JDK8)之前在HashMap扩容时,会反序单链表(是移动后看起来反链表了,而不说手动反链表的,这里要注意)
这样在高并发时会有死循环的可能,后面会说明原因
当然也有解决方式,主要是在第一个操作之后,将会循环的反向指向的那个节点的next赋值为null即可
而在JDK8中,就解决了这个问题(可能不是这个方式)
如果不知道为什么,可以看如下博客:
https://blog.csdn.net/tianc_pig/article/details/87869967
https://blog.csdn.net/qq_26012495/article/details/120836406
https://blog.csdn.net/yang553566463/article/details/108992081
好了,现在我们来编写基础代码:
来完成数组加链表,和扩容的关系,记得自己写了后,再看我的哦:
package com;


import java.util.List;

/**
 *
 */
public class testCom1 {

    //首先,我们需要一个数组(其类型是链表的节点),默认长度是8吧
    //为什么不是定义Entry呢,无论是之前还是这里,我们都是使用一个变量来指定头的,而这个变量所在的位置的类,自然就是我们要定义的类
    //我们也将这个类程序对应的数据结构,如链表
    ListEntry[] entry = new ListEntry[8];

    //定义元素占用数组的数量(即数据,可以说是数组的数,也可以说是链表的节点,即统一称为元素),不是元素数量,而是数组的数量,因为有链表的
    int size;

    //当前数组的数量的上限,我们也称为加载因子(专业术语,针对哈希表,即哈希的数组的填满冲突,就如我们飞机也会有别名一样)
    double top = 0.75;

    //我们也可以不操作这个类,都放在一个类里面但是这样是不好的,因为随着链表越长,占用的变量就越多,或者说占用的空间就越多,我们尽量减少空间
    //我们也可以发现,使用内部类可以少定义java文件,方便许多,且基本只给自己使用(使用私有的情况下,这里自然可以给其他人使用,因为是public)
    public class Entry {
        //最好是引用,因为需要调用方法,当然,如果你是基本类型也可以,但是需要装箱,这样麻烦一点
        //但是他们独有的equals是会操作成功的,只要不操作==即可,虽然Integer可能有兜底数据-128到127的数(相同的对象),我们也将他称为自动装箱池
        //且他们在这个范围里面的确==也会相等,当然,大多数是没有的兜底数据(或者自动装箱池,或者类似的)的,所以最好不要使用==(来防止相同值,但不同对象地址,导致false,比如赋值相同的值,但是结果为false,或者String操作new String也是如此,但是直接的String赋值,会始终保存的,因为常量池,所以这样基本会是为true,具体可以看20章博客)
        String key;
        String value;
        Entry next;

        public Entry(String key, String value) {
            this.key = key;
            this.value = value;
        }
    }

    //为什么这个类放在外面去了,因为这个哈希类在添加值时,需要他,否则如果他是内部类的内部类的话,要得到他需要进行嵌套(即需要ListEntry.Entry来直接使用,或者要创建对象时,需要创建链表对象来使得创建Entry对象,比如new ListEntry().new Entry,这里不写后面的参数列表了,即实在麻烦)

    //代表我们的主要信息,自然是key和value的结合
    public class ListEntry {
        //不同线程,各自的内存结构是不同的,所以基本上不会出现相同的这个head值,除非是静态的,而在实际情况中
        //比如我们操作HashMap集合,那么他通常就是静态,否则就会是不同的数组了
        Entry head;


        //由于我们只需要完成哈希表的实现方式
        //所以这里我们就不考虑非常多的细节了(当然你加上也行,这里我就加上部分细节方法),且这里只实现主要方法的链表即可,这里就直接往后面添加,且不考虑大小先后了,但是相同的还是进行覆盖(只覆盖对应的变量值,而不是整个节点)

        //我们好像并没有考虑什么线程的安全问题,因为现在还不需要考虑,我们只是学习而已,在以后你可以自己考虑

        //这里我们就不考虑head是否为null的问题,实际上我们可以在对应的操作的地方进行添加,所以能够更加的简便代码量
        //添加节点
        public void addEntry(Entry entry) {
            Entry temp1 = head;
                while (true) {
                    //这里因为只是key和value两个,而不会有其他,所以我们只需要修改value即可,而不用将该对象都进行修改
                    //实际上若没有这个覆盖的操作,其实只需要将这个节点作为头,指向即可,即放在头节点即可,就不用循环了,这样时间复杂度就是O(1)了,可是这里有,所以我们为了顺序也会顺便放在后面,而并不操作头了,当然操作头也行,虽然时间复杂度是一样,但是头的需要时间少(差别不大),因为有一个判断,代码多点,所以差别不大,通常可以忽略
                    //实际上如果操作头插入的话,再循环操作这个也行(所以也会是O(n)),代码省了一点(也就一点,因为还是存在循环的),可以忽略,且符合后插入,先查询到,虽然顺序不对
                    //也可以操作指定尾部变量来完成添加,与头插入时间复杂度基本类似,且符合后插入,后查询到,满足顺序
                    if (temp1.key.equals(entry.key)) {
                        //之所以使用equals,是防止我们赋值对象,导致==不同,因为我们只判断key是否相同,而不说地址,而equals是String是重写的方法,操作了变量的对比,而不是默认的单纯的==地址对比,所以使用equals
                        /*
                        因为如果不重写,默认是Object的:
                        public boolean equals(Object obj) {
                            return (this == obj);
                        }
                        即"=="
                        所以一般引用都是重写equals的,就与哈希函数(hashCode方法)一个都会重写的
                        实际上他们还是有联系的,一般重写的hashCode中,如果equals相等,要保证哈希值也要相等
                        当然,可能在大数据量的情况下,如果equals不相等,但是哈希值也会相等的,这是计算的原因,就如我们操作取模一样
                        单我们只需要保证equals相等,哈希值相等就行,之所以这样是为了我们自身的特点需要的(重写的独有的相等,而不说默认Object的相等,因为我们是重写的)
                        而实际上Object中==(equals)与哈希值也是一样,他们都是比较地址,所以我们基本相同


                         */
                        temp1.value = entry.value;
                        return;
                    }
                    //我们首先要判断是否相等,才判断是否为null,否则可能在第一个节点后,就添加了,而没有判断第一个节点是否相同的可能性

                    //这里不需要使用到上一个节点,所以只需要一个变量了,因为他就是往后面添加的,而不用考虑大小
                    if (temp1.next == null) {
                        temp1.next = entry;
                        return;
                    }
                    temp1 = temp1.next;
                }
           

        }

        //删除节点需要考虑没:答,可以考虑,但一般只是给内部人员使用
        //删除指定key的节点
        public void deleteEntry(String key) {
            Entry temp1 = head;
            Entry temp2 = head;
            while (true) {
                //不用考虑第一个,因为对应的调用他的方法以及操作了,即delete方法
                temp2 = temp1;
                temp1 = temp1.next;
                if (temp1 == null) {
                    System.out.println("没有该节点");
                    return;
                }
                //需要上一个节点了,所以需要两个变量来表示两个节点
                if (temp1.key.equals(key)) {
                    //后面操作时,判断了head的效果了,所以这里就这样写
                    temp2.next = temp1.next;
                    return;


                }

            }
        }

        //修改操作要考虑没:答,可以考虑,但一般只是给内部人员使用
        //修改指定key的节点
        public void updateEntry(String key, String value) {
            Entry temp1 = head;
            while (true) {

                if (temp1 == null) {
                    System.out.println("没有该节点");
                    return;
                }
                if (temp1.key.equals(key)) {
                    temp1.value = value;
                    return;
                }
                temp1 = temp1.next;
            }
        }

        //获得指定key的value的值
        public String getEntry(String key) {
            Entry temp1 = head;
            while (true) {

                if (temp1 == null) {
                    System.out.println("没有该节点");
                    return null; //null代表没有值
                }
                if (temp1.key.equals(key)) {

                    //如果temp1.value是null,这里再次的给出提示,来区分是否存在节点
                    if (temp1.value == null) {
                        System.out.println("他的值是null哦");
                    }
                    return temp1.value;
                }
                temp1 = temp1.next;
            }
        }

        //查询所有节点
        public void select() {
            String a = "";
            Entry temp1 = head;
            if (temp1.next == null) {
                a = a + "[" + temp1.key + "," + temp1.value + "]";
                System.out.println(a);
                return;
            }
            a = a + "[" + temp1.key + "," + temp1.value + "],";

            while (true) {

                temp1 = temp1.next;
                if (temp1.next == null) {
                    a = a + "[" + temp1.key + "," + temp1.value + "]";
                    System.out.println(a);
                    return;
                }
                if (temp1.next != null) {
                    a = a + "[" + temp1.key + "," + temp1.value + "],";
                }


            }


        }

        //反过来查询所有节点,由于不是双向链表,所以这里需要进行特殊改动,这里我们的改动是,定义两个字符串来解决
        public void reverseSelect() {
            String a = "";
            String b = "";
            Entry temp1 = head;
            String cc = "";

            if (temp1.next == null) {
                a = a + temp1.key;
                b = b + temp1.value;
                System.out.println("[" + a + "," + b + "]");
                return;
            }
            a = a + temp1.key + ",";
            b = b + temp1.value + ",";
            while (true) {


                temp1 = temp1.next;

                if (temp1.next == null) {
                    a = a + temp1.key;
                    b = b + temp1.value;
                    break;
                }
                if (temp1.next != null) {
                    a = a + temp1.key + ",";
                    b = b + temp1.value + ",";
                }


            }
            String[] aa = a.split(",");
            String[] bb = b.split(",");
            for (int j = aa.length - 1; j >= 0; j--) {
                if (j == 0) {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "]";
                } else {
                    cc = cc + "[" + aa[j] + "," + bb[j] + "],";
                }

            }


            System.out.println(cc);
        }


    }

    //扩容
    //将数组进行扩容两倍
    public void resize() {
        ListEntry[] numsNew = new ListEntry[entry.length * 2];
        //我们已经得到了一个新的数组,那么我们需要考虑如何将数据进行移动呢,很明显,我们需要将每个链表都进行移动
        //当然,思路有很多,比如HashMap使用为运算,来保证每个链表数组的对应位置平均,且我们可以更加的知道,他为什么是4的倍数了,因为在二进制里面,2的倍数容易操作,而他也的确这样操作了
        //相当于原来的分配更加的平均分配,为什么这样说明呢,因为我们知道4的倍数自然是2的倍数,那么在二进制中,必然是1开头的数,后续都是0
        //比如我们扩容了,那么16就是10000,但是我们通常不会操作16,因为数组下标是0,所以我们会将16-1,即01111
        //然后将得到的哈希值,与01111进行&位运算(同1为1,否则为0),如果你的哈希值为49,即110001,那么对比如下:
        //110001
        //001111  //前面补充0
        //返回的结果就是1,即放在下标为1的地方,当然,如果都没有对比成功,那么就是0了即放在0这个下标,即他刚好能代表16个数(0到15),所以2的倍数的好处就出来了,即后面的一连串的1去组合&能够很好的得出范围数据,即这里的0到15
        //且我的哈希值都是这样的操作的,与取模基本类似,他与取模的好处是什么呢,除了他与取模都是操作0到15的数外,且这个位运算和取模基本都是随机(因为不能保证整除16后是否有相同的下标,后面会说明的)的,但是位运算比较快,因为加减乘除基本都是比位运算慢的,因为位运算更加的接近底层
        //一般情况下:加(减)所用时间<乘(除)所用时间
        //位运算所用时间<乘(除)所用时间,一般情况下,除非某些底层原理不同,比如封装的位运算就摆烂,多加了一些无效的加减乘除操作,当然,基本不会的
        //实际上由于正好的翻倍的,那么通常情况下,可能不会操作分开,因为对应的位运算,需要出现剩余的1111才可,取模也是,所以,如果对应的哈希值,都能操作翻倍后的数据,那么没有变化,除非有一个没有,具体可以自己测试
        //比如:
        /*
        int index = Math.abs(3334% 16);
        System.out.println(index);
        index = Math.abs(3334 & 16-1);
        System.out.println(index);
        index = Math.abs(3334%8);
        System.out.println(index);
        index = Math.abs(3334 & 8-1);
        System.out.println(index);
        //上面的结果都相同,都是6,因为3334,既可以无论是操作16还是8,都只会剩下8
         int index = Math.abs(333% 16);
        System.out.println(index);
        index = Math.abs(333 & 16-1);
        System.out.println(index);
        index = Math.abs(333%8);
        System.out.println(index);
        index = Math.abs(333 & 8-1);
        System.out.println(index);
        //上面的结果不同,操作16的是13,而操作8的是5,因为333,操作16剩下13,而操作8剩下5,因为13能操作8了但不能操作16,所以需要对应的哈希值必须没有对应倍数的关系(即余数必须只能多或者有一个8,比如有13),即不能操作翻倍后的数据,才基本会进行分配,否则基本不会发现下标改变,比如22除了16,即6,而22除了8,也是6,如果是30,那么除了16就是14,而除了8还是6,这是对位关系(如果是16,和32,那么32就考虑余数只能多一个16了)
        从上面可以看出,有些时候,可能并不会操作平均,且对应的位运算与%的操作结果基本类似,而又因为提高的运行效率,所以我们尽量使用相同结果的位运算
        从上面我们也能得出他的分配平均度,自然的分配到的其他地方是15-8(包括15和8),即8个下标
        很明显,他们对应的下标与原来的8长度数组的下标是相对应的,即如果我的8是,那么你必然是0
        所以这个分配我们也通常叫做:对位分配
        所以他们也并不是随机分配的,或者说平均分配的,而是对位的
        且我们也知道对位是与原数组的对位,所以再次的翻倍后,就是16与32对位了,这时我们也要考虑他们之间的对应倍数的关系来对位分配了(即余数必须只能多或者有一个16,比如有30),即不能操作翻倍后的数据,才基本会进行分配
        */
       
        //既然思路有很多,那么我们使用那一个呢,既然位运算比较好,那么我们就使用位运算
        size = 0;
        //首先遍历所有数据,然后重新分配
        for (int i = 0; i < entry.length; i++) {
            //这里也需要考虑一个问题,我们是移动整个链表,还是每个节点都分别移动
            //如果是整个链表,我们可以只考虑首个节点的key然后移动即可
            //如果每个节点移动呢,是从头部先移动,还是从尾部先移动,如果是头部,那么很明显,直接移动即可
            //如果是尾部,可能需要更多的时间复杂度,但是他是单链表,我们基本上能够保存上一个节点就需要一个变量了,而如果都要保存,可以也是可以,只是需要很多变量,且链表长度不一(所以我们一般不会这样操作,因为基本操作不了,长度会变化的,而代码不会,所以变量也不会,即操作不了)
            //所以单纯的设置变量不符合实际,那么基本上只有一种可能(可能有其他方式),即多次的循环得到后面的节点,但是这样的话,会需要两个循环(一般是嵌套循环),时间复杂度变得很高,不友好
            //这里好像并没有什么反链表的操作,实际上的确没有,因为通常是自动的出现的(移动造成的)
            //那么为什么要操作反链表呢:首先看如下,如果我们需要先操作尾部节点,那么这个时候我们需要反链表了,比如上面的每个节点移动从尾部开始
            //你可能会这样的认为,我们操作头节点不就可以了,的确,这样认为也是对的,但是,链表也是有不同的,在前面我们也知道,我们操作了一个小的在前,大的在后的链表,如果我需要一个方向的打印
            //那么完全可以先反链表,在操作打印,这样就比原来的代码少了很多,且循环变少(去除","也算一个循环)
            //实际上在散列表中,操作移动基本会自动的出现反链表,而不是我们手动的操作,看如下例子:
            if (entry[i] != null) {
                //这里我们考虑每个节点都移动,且是从头节点开始,因为头节点需要的时间复杂度低
                Entry temp1 = entry[i].head;
                while (true) {

                    if (temp1 != null) {
                        int index = temp1.key.hashCode() & (numsNew.length - 1);
                        if (numsNew[index] == null) {
                            ListEntry list = new ListEntry();
                            list.head = new Entry(temp1.key, temp1.value);
                            numsNew[index] = list;
                            size++;
                        } else {
                            //我们可以很明显的发现,我们每次都是将原数组的头进行放入,那么在新的数组中,就变成了反链表,那么这里再多线程下,会出现死循环
                        //特别的是这一步,这里的节点移动是将对应的属性进行移动
                            //虽然节点移动需要创建对象,浪费空间,但是基本是不会出现死循环的,各有利弊吧
                            //如果这里不是操作新的节点,而是进行地址移动(不是节点移动),如果进行地址移动,那么对应的下标,就是等于我们的temp1,但这样就相当于是整个链表移动过去了,所以需要设置next为null,但是在这之前,我们可能先得到他的next,然后至此,在实际操作之前,如果其他线程还没有进来的,但可能对应的头和头的next还是被得到的,以此为例,然后我们再操作下一个时,如果他正好操作的哈希值也是对应第一个的下标,那么自然,需要进行从头插入,如果从尾部插入,那么需要循环,为什么这里的头不用循环了,因为没有判断是否相同的了,因为原数组已经操作了,所以从头插入实际复杂度低
                            //所以这里我们操作从头插入,那么我们可以发现,形成了反链表,那么有个问题,如果我们操作完毕,或者对应的指向操作完毕(比如第二个指向第一个了,当然其他的指向也是一样,只要刚好被改变就会发生,比如一开始相同,并驾齐驱,然后突然一个快了点,改变了,那么慢点的就会发生死循环),当其他线程进来,那么由于前面我们知道其他线程如果得到了他的头的下一个,也就是第二个,很明显他的指向已经变了,那么当我们操作明面上的他们时,将头先放入,然后操作该变量,很明显,又操作第二个变量,然后他的下一个也是头了
                            //按照正常的来说,你肯定会认为他放完这个头后,会结束,因为后面就是null吗,但是他确有个缺陷,而正是这个缺陷,导致出现了死循环,由于是从头插入那么第一个先放入,然后用一个变量指向他,然后第二个再放入,第二个指向他,但是第二个的下一个是next,那么第一个再放入,那么第一个指向第二个,我们可以发现,第一个指向了第二个,而第二个又指向了第一个,我们很自然的将原来第一个的next的null变成了第二个了,那么他们会永久的这样下去,所以这里的缺陷就出来了,如果你看这些解释并不明白,那么可以到如下博客查看:https://www.cnblogs.com/yaphetsfang/articles/12172848.html,既然会出现一个死循环,那么在这个基础上,可能我们在读取时,也会发生死循环,即造成一系列的问题,比如也是刚好得到对应的第一个和第二个,且都没有符合key的,那么会出现死循环,所以不只是对应的添加元素会发生死循环,在扩容时,其他操作可能也会发生死循环
                            //所以说在多线程下或者说高并发下,其中一个线程,会操作反链表,特别的,如果他们刚好都进入这个方法里面,即比较容易出现死循环(即高并发的情况下容易出现死循环),所以一般情况下,如果是jdk8版本以前,我们最好不要操作多线程的HashMap,防止这种情况,当然,也有很多修改方式,jdk8及其以后就进行了改变,我这里也进行了改变,看如下:
                            
                            //而我这里直接的创建一个节点,那么就不会出现对应死循环的问题了,因为我们并没有将链表进行改变(主要的是链表节点,因为线程是先得到节点的),而不是节省创建对象的资源来进行链表继续利用,所以原链表的节点指向没有被改变
                            //当然,如果你在线程得到节点之前不允许操作也是可以的
                            
                            numsNew[index].addEntry(new Entry(temp1.key, temp1.value));
                        }
                         
                        temp1 = temp1.next;
                    } else {
                        break;
                    }
                   
                }


            }

        }
        entry = numsNew;


    }

    //上面的链表我们定义完成,接下来,就是判断哈希值了
    //往链表数组里添加节点(由于数组的值是链表,我们也称该数组是链表数组)
    public void put(String key, String value) {
        //为什么这里不是节点,实际上比如是HashMap的话,首先,对应的有些方法是私有的或者是内部的或者权限
        //具体看访问权限,而我们使用时,基本只能使用public的权限,其他的需要在他的包或者有联系,必然继承里
        //即一般不能被得到,所以不能直接被使用,这里就不修改了
        //所以外面基本是不能直接的传递节点,因为你需要创建啊,而他却是内部类或者需要权限,那么通常是操作不了的

        //接下来,我们给这个key操作哈希值
        int index = Math.abs(key.hashCode() % entry.length);
        //下面基本会进行赋值,但是可能只是加在链表上的,所以需要判断
        //我们创建好后,给数组赋值
        if (entry[index] == null) {
            //首先我们需要定义一个链表,而该链表在类里面,那么自然可以不用考虑权限的问题,所以通常可以直接定义,而在其他地方需要考虑,虽然这里是public
            ListEntry entry1 = new ListEntry();
            entry[index] = entry1;
            //如果该位置的数组的链表是第一次添加了,那么该链表的head基本是null,所以我们给他节点,这个节点与该链表一样,在类里面,所以通常其他人也不能直接获取,虽然这里是public
            //至此,我们得到了数组的下标并给其赋值了链表,在前面我们说过,我们需要在这里进行给头(head),因为前面我们没有判断头是否是null的,所以这里需要进行操作
            entry[index].head = new Entry(key, value);
            //实际上这样也保证了,如果不加数据,那么数组对应值为null,否则必有一个head,即实际上获取数据时基本没有单纯的head==null的出现
            //而正是这样,所以我们不用考虑head为null的情况了,也就节省了多个方法要判断的代码量,自己可以看之前相关链表操作中的判断代码即可

            //只有这个,才算是一个新的元素,因为占用了数组的下标,所以size++,否则就是操作链表,不用size++
            size++;
            //那么加载因子和初始数组大小有什么关系呢,为什么我们可以发现,有些哈希表加载因子是0.75,数组初始大小是4的倍数呢,比如HashMap的加载因子是0.75,数组大小是16
            //为什么要这样设置,这里我们给出解释
            //首先是加载因子,为了不让数据过少而扩容,也为了不让数据过多而不扩容,我们通常会进行折中,即0.75是非常好的数字,即大多数都会使用0.75
            //那么在0.75的基础上,我们需要考虑初始数组了,接下来看如下:
            //如果10*0.75会得到什么呢,10*0.75=7.5,会发现有小数点,实际上这是不好的结果,因为下标可是没有小数点的
            //所以我们就会强转,那么自然的就会违背加载因子的意思,因为你省略的小数,对于整体来说,相当于不是加载因子了
            //比如对于10来说,0.70与0.75是相同的,即对于精确度来说是不精确的,而为了不出现小数,则必须是4的倍数,这样才能保证刚好是0.75倍而扩容,而不是到0.70就扩容了
            //因为0.75就是4分之3,只有是4的倍数,才会出现整数,否则除以4是比如会出现小数的
            //所以可以得出,一般来说,加载因子是0.75的,对应的数组是4的倍数,当然,虽然0.75是折中,但某些情况下,可能是不同的加载因子,那么就看加载因子的分母来决定数组倍数了
            //但是我们最好使用0.75,因为对应的倍数4不只是满足他的加载因子,也满足位运算中,2的倍数(比如HashMap扩容时操作的位运算),所以我们最好使用0.75和倍数4的组合关系,当然其他的关系也行,只是0.75和倍数4的关系基本上是非常好的,有好的为什么不用好的呢,你说是吧


            //代表满足加载因子,而扩容
            if (size >= entry.length * top) {
                resize();
            }
            return;
        }

        //直接的指定添加方法,那么由于上面已经判断了是否为null了,所以对应的方法可以不用判断
        entry[index].addEntry(new Entry(key, value));


        //至此,我们可以来总结一下:很明显,我们操作的类似于哈希表的类,实际上只是确定在那个数组里加上链表,而确定后,就单纯的是链表操作了

        //所以如果你不操作内部类,那么需要创建三个文件,一个是实体类文件,一个是链表类操作文件,一个是哈希表类文件(即操作key-value的类)
        //其中,哈希表类文件确定数组位置,自然需要链表类文件,还需要节点,即也需要实体类文件,然后他先将链表类赋值给数组,然后操作链表进行其链表操作
        //如添加节点,在前面的测试中,所以我们也会知道,main方法里面都会创建一个对象,而该对象就是链表类(这样称为的)
        //而链表类文件,自然需要实体类文件操作其在的各种操作方法,这就是大致的思路,实际上很简单的
    }

    //获取指定key的value
    public String get(String key) {
        //自然我们首先需要得到下标
        int index = Math.abs(key.hashCode() % entry.length);
        //然后判断对应的下标是否有数据
        if (entry[index] == null) {
            System.out.println("没有该key指定的value数据");
            return null;
        }
        //记得,我们传入的key只是确定了下标,但是key本身是没有变了,所以链表才会有多个
        String value = this.entry[index].getEntry(key);

        return value;
    }

    //大多数情况下,哈希表(是key-value的链表数组也称为哈希表,只要是类似于key-value结构即可,都能称为哈希表)只会给出添加方法和获取方法
    //现在我们来手动添加修改和删除

    //修改指定key的value
    public void update(String key, String value) {
        //自然我们首先需要得到下标
        int index = Math.abs(key.hashCode() % entry.length);
        //然后判断对应的下标是否有数据
        if (entry[index] == null) {
            System.out.println("没有该key指定的value数据");
            return;
        }
        //记得,我们传入的key只是确定了下标,但是key本身是没有变了,所以链表才会有多个
        this.entry[index].updateEntry(key, value);

    }

    //删除指定key
    public void delete(String key) {
        //自然我们首先需要得到下标
        int index = Math.abs(key.hashCode() % entry.length);
        //然后判断对应的下标是否有数据
        if (entry[index] == null) {
            System.out.println("没有该key指定的value数据");
            return;
        }
        //这里有个问题,删除后,如果该节点就是head,那么他会设置为null,而我们并没有进行判断,所以如果这时候我们添加数据时
        //由于该数组位置有数据(只是只是head为null,而不是数组下标指向变成null),那么自然会添加,而添加里面没有判断head是否为null,自然会容易出现空指针异常
        //所以这里需要进一步的修改
        if (entry[index].head.key.equals(key)) {
            if (entry[index].head.next == null) {
                entry[index] = null;
            } else {
                entry[index].head = entry[index].head.next;
            }
            return;
        }
        //记得,我们传入的key只是确定了下标,但是key本身是没有变了,所以链表才会有多个
        this.entry[index].deleteEntry(key);

    }

    //现在,我们可以编写一个查询所有的方法,很简单的
    public void select() {
        System.out.println("正向查询所有:");
        System.out.println("链表数组长度是:" + entry.length);
        for (int i = 0; i < entry.length; i++) {
            System.out.print("第" + i + "个链表:");
            if (entry[i] == null) {
                System.out.println("[]");
            } else {
                entry[i].select();
            }
        }
    }

    //反向查询
    public void reverseSelect() {
        System.out.println("反向查询所有:");
        System.out.println("链表数组长度是:" + entry.length);
        for (int i = 0; i < entry.length; i++) {
            System.out.print("第" + i + "个链表:");
            if (entry[i] == null) {
                System.out.println("[]");
            } else {
                entry[i].reverseSelect();
            }
        }
    }


    //程序的入口,这是规定的,当然,既然是规定,那么必须是这样写,如果是private,没有执行按钮,即方法没有声明,对应执行器找不到当前的main(需要public才可,规定的,执行器也要权限,或者说就是规定)
    //当然,也要注意一点:有些文件夹里面不能修改文件,保存也没有用(虽然打开后看起来是修改了,但实际上没有),所以需要注意(特别是c盘相关文件,一般是设置成这样的,可能也可以修改,这里只是提一下)
    public static void main(String[] args) {
        testCom1 t = new testCom1();
        //很明显,他里面有一个数组,而定义的是链表数组

        //添加节点
        t.put("1", "2");
        t.put("2", "3");
        t.put("2", "34");
        t.put("2", "343");
        t.put("22", "3");
        t.put("223", "3");
        t.select();
        t.reverseSelect();
        t.put("3", "3");
        t.put("4", "3");
        t.put("5", "3");
        t.put("6", "3");
        t.put("7", "3");
        t.select();
        t.reverseSelect();
        t.delete("2");
        t.select();
        t.reverseSelect();
        t.update("22", "334");
        t.select();
        t.reverseSelect();
        String s = t.get("1");
        System.out.println(s);

    }


}

//至此,我们独有的哈希表就编写完成
//我们可以发现,由于引用自带重写哈希函数,所以对应的key或者value可以操作泛型,比如你可以将key设置位Object来操作,会发现,结果是一致的,因为哈希函数是对象来调用的,而不是引用,引用只是帮对象来调用而已,主要是保存的意思
时间复杂度:
写操作:O(1) + O(m) = O(m),m(可以认为是n,随便认为,只要是未知数即可)为单链元素个数,因为我们只是执行了对应的方法(put),而该方法就操作了循环
很明显他的意思是哈希表的添加方法,而不是单独的添加操作
读操作:O(1) + O(m) = O(m),m为单链元素个数,很明显他的读操作也是哈希表的方法(get)
为什么上面要加上O(1)呢,因为对应的方法,是可以出现不循环的,或者进入循环,但只执行一次就退出的,而没有操作循环
所以有时候是O(1),但是整体来说,是O(n),因为我们需要整体化,前面已经说明过了
Hash冲突写单链表:O(m),代表如果发生冲突了,那么就放在链表后面,自然就是代表方法(addEntry)
Hash扩容:O(n),n是数组元素个数(针对于整个链表移动),如果是移动单个节点而不是链表,那么需要O(n^2),前面代码的就是这个
但是更加的平均了,只是需要的时间比较多
Hash冲突读单链表:O(m),m为单链元素个数,自然就是代表方法(getEntry)
注意:上面的意思基本是方法,而不说某个独立(单独或者单纯)操作
优缺点 :
优点:读写快(一个key对应一个value),比单纯的使用数组(即相当于开放寻址法,往后面增加),要快
缺点:哈希表中的元素是没有被排序的、且会发生Hash冲突和扩容重新计算
应用:
针对于数据对应来说,我们需要一个key对应一个value,而不是通过下标来对应数组或者链表(一般通过第几个),虽然链表在这之前,我操作了id来查询,但是实际上id他自身也是数据,即有了key和value的思想了,所以实际上链表只能通过下标(即第几个来表示),就算链表可以,但是也可以发现,相当于开放寻找法,因为无论怎么样,添加数据时,都需要循环判断是否覆盖的原因,当数组越来越大时,就越慢,但读取也要循环整个链表,即可以发现,数组好点,因为数组只判断后面的,即总之来说,上面的key和value模式,是比单纯的数组和链表要快的
比如:
HashMap:
JDK1.7中HashMap使用一个table数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置
如果hashcode相同,或者hashcode取模后的结果相同,那么这些key会被定位到Entry数组的同一个格子里
这些key会形成一个链表,在极端情况下比如说所有key的hashcode都相同,将会导致这个链表会很长
那么put/get操作需要遍历整个链表,那么最差情况下时间复杂度变为O(n),虽然基本是这样
扩容死链:针对JDK1.7中的这个性能缺陷,JDK1.8中的table数组中可能存放的是链表结构,也可能存放的是红黑树结构
如果链表中节点数量不超过8个则使用链表存储,超过8个会调用treeifyBin函数,将链表转换为红黑树
那么即使所有key的hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(logn)的开销,以后会说明红黑树

在这里插入图片描述

字典:
Redis字典dict又称散列表(hash),是用来存储键值对的一种数据结构(通常是链表法)
Redis整个数据库是用字典来存储的(K-V结构)
对Redis进行CURD操作其实就是对字典中的数据进行CURD操作
Redis字典实现包括:字典(dict)、Hash表(dictht)、Hash表节点(dictEntry)

在这里插入图片描述

布隆过滤器:
布隆过滤器(Bloom Filter)是1970年由布隆提出的,它实际上是一个很长的二进制向量和一系列随机hash映射函数
布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间都远远超过一般的算法

在这里插入图片描述

布隆过滤器的原理是,当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个数组中的K个点,把它们置为1
检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:
如果这些点有任何一个0,则被检元素一定不在,如果都是1,则被检元素很可能在,这就是布隆过滤器的基本思想
位图:
Bitmap 的基本原理就是用一个 bit 来标记某个元素对应的 Value,而 Key 即是该元素
由于采用一个bit 来存储一个数据,因此可以大大的节省空间
Java 中 int 类型占用 4 个字节,即 4 byte,又 1 byte = 8 bit,所以 一个 int 数字的表示大概如下

在这里插入图片描述

试想以下,如果有一个很大的 int 数组,如 10000000,假设数组中每一个数值都要占用 4 个字节(b),实际上也是如此
则一共 需要占用 10000000 * 4 = 40000000 个字节,即 40000000 / 1024.0 / 1024.0 = 38 M(约等于)
如果使用 bit 来存放上述 10000000 个元素,只需要 10000000 个 bit 即可
10000000 / 8.0 / 1024.0 / 1024.0 = 1.19 M 左右(先除以8表示先变成对应的字节),可以看到 bitmap 可以大大的节约内存
使用 bit 来表示数组 [1, 2, 5] 如下所示,可以看到只用 1 字节即可表示(用比特中特定的1表示位置值,如key代表5,而value就是该位置的值,即1,通常可以用来操作是否存在的意思):

在这里插入图片描述

即他将字节当成数组,而不是将数组的值用字节保存,该方法一般来判断是否存在的意思,而不是存放具体的value
因为value基本只有1和0
当然上面的只是示例而已,实际上是需要看数组类型的,而使得一个数组的下标占用多少个字节
现在我们开始说明递归
递归:
概念:
递归,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法
也就是说,递归算法是一种直接或者间接调用自身函数或者方法的算法

在这里插入图片描述

本质:
递归,去的过程叫"递",回来的过程叫"归"
递是调用,归是结束后回来,他是一种循环,而且在循环中执行的就是调用自己,一般递归调用将每次返回的结果存在自身所在的栈中(不是生成的),我们一般将生成的称为栈帧,所以我们操作的是一个大栈,而我们的代码操作都是在这一个栈里面操作栈帧(也是栈,可以有多个存储,如多个变量,开辟的栈)
递归三要素:
1:递归结束条件
既然是循环就必须要有结束,不结束就会OOM了,即内存溢出(Out Of Memory,简称OOM),始终使用内存,导致没有内存了
指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存(因为已经占满了,没有提供的内存了),到那时,就会报错(底层的监测,这不用考虑,注意即可)
2:函数的功能
这个函数要干什么,打印,计算等等
3:函数的等价关系式
递归公式,一般是每次执行之间,或者与个数之间的逻辑关系,比如f(n)=f(n-1)+1,典型的就是数列,比如1,2,3,4,5
即f(n)=f(n-1)+1,(n>1),n代表第几个数,对于数学来说,数列就是最好的例子
小例子:
打印5次"Hello World":
package com;

/**
 *
 */
public class recursion {

    //循环实现
    public void round() {
        for (int t = 0; t < 5; t++) { //实际上循环可以看成执行次数,那么只要次数对应即可,并不是非要是0和5,就算是3和8也行,这是数据结构的一个自由选择,相当于你先加1然后减1,或者先减1然后加1一样,看你自己如何操作,但结果都是1(5次)
            //那么以这样的理论,我们看具体操作或者需求时,也要看循环次数,比如后面的递归实现,来决定输入的下标后的结果是否符合预期
            System.out.println("Hello World");
            //实际上循环是需要确定次数的或者单独的操作,如果某些操作不能确定次数,或者无论怎么变化以及循环不好操作(比如分开的操作),那么可能是使用不了循环的,所以循环不是万能的,具体的例子可以去网上找,但是实际上大多数操作(基本上都可以,只要是多次操作的就能使用循环)都能操作循环,因为可以操作循环嵌套
            //其中后面的斐波那契数列,也可以使用循环,且使得是O(n),后面有具体代码
            //所以在前面我也说过,循环包括很多类型,而不只是对应的循环语句,这里要注意,如不只是for循环语句,这里只要是多次的操作都称为循环,比如for循环语句,数组扩容,递归等等,当然还有其他的,就不依次说明了
            //这些操作都是算法的一种,来操作数据结构(如赋值)
        }

    }

    //递归实现,每一个调用(函数或者方法)都会有独有的栈出现,通常首先在main方法的里面操作执行该函数(方法)后才会出现,只有他们的方法,都执行完毕,才会删除该栈或者栈帧出栈(不是单纯的出栈的意思,是大栈出栈,这里就说明删除栈),即如果第一次调用的recursion方法执行完了,那么该函数开辟的栈才会删除,但是他里面也有方法,那么也需要等待他里面的方法(他独有栈)删除栈才可算自己执行完毕,才能删除栈
    
    //简单来说,就是以栈来操作循环,而不是用循环语句操作循环,主要的好处是,可以循环的利用该方法的操作,而不是单纯的操作类似于for语句或者其他语句里面的操作,虽然需要更多的空间,但是可以利用更多的操作
    public void recursion(int n) {
        if (n <= 0) { //最好设置位<=0,万一出现问题,那么会大大的浪费内存的,栈需要内存的,栈放在大栈(一般大栈如果满了,底层通常就会扩容大栈,但是内存没有了,自然OOM了,即内存溢出,这时通常会报错)
            //在程序里面的基本上的任何操作,都需要内存,除非有操作硬盘的,如文件,但是他们的操作也需要内存,只是将内存数据使得到硬盘里面了
            return;
        }
        System.out.println("Hello World");
        recursion(--n);
    }

    public static void main(String[] args) {
        recursion r = new recursion();
        r.round();
        System.out.println("-----");
        r.recursion(5);
    }
}

递归要素:
递归结束条件:n<=0
函数的功能:System.out.println(“Hello World”)
函数的等价关系式(即再次调用函数的关系,而不是等值):recursion(n)=recursion(–n)
经典案例:
斐波那契数列:0、1、1、2、3、5、8、13、21、34、55…
规律:从第3个数开始,每个数等于前面两个数的和
递归分析:
函数的功能:返回n(下标,这里从0开始,从1开始也行,只需要改变if (n <= 2) return n-1;即可,然后下面输入的6变成7就行,看下面的代码就知道了)的前两个数的和
递归结束条件:对于第几个数来说,就从第三个数开始(不包括第三个数,因为0+1=1),之前的数就直接返回,对于下标来说,就是n<=1
函数的等价关系式:fun(n)=fun(n-1)+fun(n-2)
案例:
package com;

/**
 *
 */
public class test {
    public int fa(int n) {
        if (n <= 1) return n;
        return fa(n - 1) + fa(n - 2); //无论你怎么操作,最终都会使得以0和1的数一步一步的往上加
        //很明显,他们的递归关系,与数学是有关联的,所以最好先分析好数学
        //因为虽然程序里面的n可能需要到最底层的直接返回,但无论他怎么操作,都一定会得到数据
        //就与数学一样,如果在数学上,你操作斐波那契数列,你知道了他的公式,但是只给你前两个数,那么要你求出他的第60个下标的数,自然,通常也要一步一步的进行操作(如果有某些通项公式那么就能够更加的简便了),与程序一样,只是我们自己很难算出,而程序容易,所以程序对我们数学的计算是非常好的,当然,如果你有好的办法,那么也可以在程序里面来实现
        //即程序里面能使用到数学的地方,自然也会因为数学而可以进一步优化(比如知道了通项公式,如果数学可以的话,或者很厉害的话,那么可以看这个博客:https://zhuanlan.zhihu.com/p/352341370),当然,他们都是使用数据结构来完成的(赋值也算),所以无论是使用了数学还是没有使用数学,都可以使用数据结构来完成优化(只是有上限而已)
        //那么我们来试一试,如果能简便的得出第60个下标的数呢,除了数学的办法,实际上有很多方式
        //我们可以参考这个博客:https://blog.csdn.net/qxhly/article/details/105328605
    }

    public static void main(String[] args) {
        test t = new test();
        int fa = t.fa(6); //指定下标6(0开始)
        System.out.println(fa); 
    }
}
//然后你可以试着将6修改成50(可能超过了int,导致结果不对,将方法和接收的类型修改成double即可),可以发现,非常的慢,如果使用数学的方式,或者其他优化的方式就非常快了,特别是数学的方式,所以也更加的证明数据结构存放好坏的上限以及循环的上限了

//所以这里给出数学的作用,代码如下:
package com;

/**
 *
 */
public class test1 {
    public Double fa(int n){
        return (1/Math.sqrt(5))*(Math.pow(((1+Math.sqrt(5))/2),n)-Math.pow(((1-Math.sqrt(5))/2),n));
    }
    //一般来说,如果出现1.25E10,的类似情况,代表1.25*10^10(次方)
    public static void main(String[] args) {
        test1 t = new test1();
        System.out.println(t.fa(6)); //可能有误差这是因为程序的问题(特别是根号,基本没有什么解决的方式,因为就是二进制的精度问题一样,相当于十进制的精确问题,在数学上,我们可以直接的表示根号,但是在程序里,必须是一个值,自然会操作省略一些(即取有限的小数),使得其中的值的精度(代表与原数据的差别,精度越小,越靠近原数据)出现问题,从而使得结果也出现误差),即小数的问题,不可能是无限的,所以有误差,但是区别不大
        //而正是因为区别不大,所以我考虑将他最近的整数取出,比如(可能手欠,打出必然)6.6=7,6.3=6,即四舍五入的意思
        System.out.println(Math.round(t.fa(6))); //这样基本就真正的实现斐波那契数列的O(1)了
    }
}
//这样,我们就不用递归了,直接的得到结果(时间复杂度大大降低,即需要的时间大大减少),虽然有小小的误差,自己可以进行测试

//那么什么是精度问题呢:这里需要考虑进制的问题,首先1/3可以除尽吗,答:除不尽,之所以除不尽主要是余数循环的问题(自己除就知道了)
//大多数都是如此,而在数学上我们可以直接用三分之一来表示,但在程序里,必然需要省略一些(即取有限的小数)
//任何数据都有一个除不尽的数,所以除不尽只与数据有关,那么二进制也是如此
//这里先给出对应的十进制到二进制的转换问题,首先是如下:
//将十进制变成二进制,以11为例子:
//2表示转换的进制
11/2 = 5 //余数为1
5/2  = 2 //余数为1
2/2  = 1 //余数为0    
1/2  = 0 //余数直接为1
//从后面开始,就是1011,即就是十进制的11,为什么要从后面开始呢:答:既然是进制问题,那么必然是逢10进1,很明显,这里的11是刚好进了1的,所以为了变成2进制,我们需要将他的这个逢10分解成逢2,那么总体来说,就是将十进制的数进行查看,看看有几个等级2(不同等级的2是不同的,因为是逢2进1,那么第二个逢2需要第一个逢2操作完后才可逢2),所以自然需要将数进行除2划分,而余数代表第几个,很明显,如果你除了多,那么自然最后一个的余数代表最后面一个,所以从后面开始
//那么我们反过来,1011二进制如果变成十进制呢:
//很明显,同样的理论,我们将逢2分解成逢10,那么我们需要将他们都看成数即可
//即:
1*2^0 + 1*2^1 + 0*2^2 + 1*2^3 = 1+2+8 = 11 
//简化为:
2^0 + 2^1 + 2^3 = 1+2+8 = 11 //只看有1的数来操作次方
//即将他们都看成10进制来算,2表示当前进制
//注意:因为我们熟悉10进制,上面的操作才这样的,这也是因为10进制是最容易算的而已,所以实际上我们在计算过程中,十进制是贯彻整个过程的,比如8,比如5/2等等,这些都是建立在10进制为基准的操作下的
//而如果是11进制,那么前面的11/2就是10/2,而11就是10,这个10你要强行认为是11
//即虽然上面的操作是建立在10进制上的,但实际上高变低,使用第一个,而低变高使用第二个,适用于任何进制


//上面都是操作大于等于0的,那么小于0呢,如果是0.5十进制,那么其二进制是什么,在这之前,我们可以假设,十进制的数是A
//而二进制的数是11111,那么结果就是:
A=2^0+2^1+2^2+2^3+2^4 //那么由于是等于关系,所以我们可以任意的操作
//比如:
5 = 101
5 = 2^1 + 2^2
5/10 = 2^1/10 + 2^2/10 = 0.1 + 0.4 = 0.5 = 2^-1 //即由于变成二进制的换算是2^-1,所以我们说,二进制的0.1代表十进制的0.5,因为0是2^0,那么二进制0.1我们就认为是2^-1(注意:这里是十进制的操作,不同进制的操作是不同的,因为十一进制的计算可不是这样的哦,特别是0.5可能不是0.5,那么结果自然不是0.1),所以通过这样的说明,那么二进制的0.11,如果表示呢
//我们可以发现,2^-1+2^-2 = 0.5+0.25 = 0.75,所以0.11二进制变成10进制就是0.75
//即我们将二进制变成十进制与大于0的方式一样即可,即使用上面的方式来计算,也符合低到高的标准
//那么我们为什么将2^-1认为是0.1呢,实际上就如1+1我们规定等于2一样,他也是规定(显示),规定负数次方在点"."后面操作,即以点"."进行分开,即我们也可以发现,一个1就代表除一次2,他们也有2的倍数哦,那么由于不是小数的是相加,所以他也是相加,所以0.11(二进制)就是0.25加0.5=0.75(十进制),后面会说明为什么
//现在可以理解:2x/2=x,无论x是否是负数,所以不是小数的操作,也适用于是小数的操作,而点"."只是用来区分是否是小数的转换而已
//当然,总之他们是小数二进制和十进制基本都是规定的(显示),而之所以规定,是用小数来代表负次方,虽然他们是根据不是小数的操作来表示的,但是该显示还是规定的(即".",既然这样,你也可以认为是其他符号,如",",但是由于十进制也是小数点,所以通常我们二进制也表示小数点)

//那么十进制到二进制呢,特别的前面的取余数可以吗,我们可以来尝试一下,如果是0.5,那么是如下:
0.5/2 = 0.2 //余数0.1
0.2/2 = 0.1 //余数0
0.1/2 = 0.05 // 余数0
......
//我们可以很明显的发现,由于操作小数了,那么后续可以无限的扩展,即上面的方法不管用了
//我们继续观察上面的方法,他首先是操作到0,才结束的,那么我们操作到什么才结束呢
//实际上我们可以观察A=2^0+2^1+2^2+2^3+2^4 = 11111(二进制) = 31,可以更加的理解之前十进制要将后面作为第一个,因为2^4可以被除4次(除到0),代表他的1所在的位置是第5个1(0也算一个,所以是5),所以可以这样认为,谁先到2^0的进行放入,然后舍弃(没有则置为0),后到的放在前面(中间没有的,则放入0),每次只除一次,放好后再除,直到没有值了,即都舍弃了,就结束这个操作,那么结果就是其二进制,即1(2^4)1(2^3)1(2^2)1(2^1)1(2^0),这个先放入后放入,就与之前的余数先放入和后放入基本是类似的(实际上就是一样,只不过是不同的表达方式而已,就如1+1=0,也可以表达成2-1=0,但结果都是0,即都是一样的,只是表达方式不同),所以他们之间还是有理论关系的,因为本来都是根据逢谁分解成逢谁的转换规则的
//那么我们假设,0.75 =2^-1 +2^-2 = 0.11(二进制),正是因为上面的关系,所以我们认为可以这样,我们进行相乘,谁先到2^0,谁先放入,然后舍弃(没有则置为0),后到的放在后面(中间没有的,则放入0),每次只乘一次,放好后再乘,直到没有值了,即都舍弃了,就结束这个操作,那么结果就是其二进制
//那么很明显,0.75的结果是0.1(2^-1)1(2^-2),对于小数来说,后面与不是小数的是相反的
//所以我们也得出一个方便的操作,即0.75*2 = 1.5,很明显出现的1就是2^0,因为其他的都是小数(因为负数次方的存在),所以有如下:
0.75 * 2 = 1.5 //余数1(不是小数点的),舍弃,变成0.5
0.5 * 2 = 1.0 //余数1,舍弃,变成0,结束
//即加上两个余数,就是0.11了
    
/*
总结:
十进制到二进制(高到低都行,但要注意计算)
大于等于0,使用除2的方法
小于等于0,使用乘2的方法
二进制到十进制(低到高都行,但要注意计算)
直接使用次方即可

所以如果程序出现了精度问题,那么必然是因为其转换的原因导致的(即数字本身存在小数的问题),所以特别是有小数的十进制到二进制的数,容易出现精度问题,因为需要操作小数,自然就是操作小于0的进行转换
因为其他的基本上是不会的,(即低到高基本不会,大于0的高到低基本不会)
*/
时间复杂度 :

在这里插入图片描述

斐波那契数列的普通递归解法为O(n^2),因为我们每次将n增大1,那么可以看到对应的图像中,会多出一条线
即翻了倍(不完全翻倍,因为其中一个是另外一个的其中一个,比如(有些地方可能会打成必然,这是我没有注意的原因)20对应19和18,那么增大1后,21对应20和19,即对应19和18,加上多余的19(21对应的),只是多出一个19而已,而不是多出一个20,所以是部分增大,即在原来的翻倍少了一个18,即不是2个19和2个18,而是这里的2个19和1个18)
实际上如果使用循环的话,那么是O(n),所以使用的算法也是有好有坏的哦(这里就是循环要好),很明显,他们都是指方法,大多数情况下,我们都会认为是方法,除了某些特殊说明,如链表的插入,但我们也统一认为是方法
循环的操作斐波那契数列的具体代码如下:
package com;

/**
 *
 */
public class tt {

    public static int fa(int i) {
        if (i <= 1) return i;
        int a = 0;
        int b = 1;
        for (int j = 2; j <= i; j++) {
            b = b + a;
            a = b - a;

        }
        return b;
    }

    public static void main(String[] args) {
        System.out.println(fa(6));
    }
}

递归的优缺点 (这里只说明递归),因为循环没有什么说明的,因为是基础算法
优点:代码简单
缺点:占用空间较大、如果递归太深,可能会发生栈溢出、可能会有重复计算,可以通过备忘录(动态规划,在说明动态规划时会说明)或递归的其他方式去优化
应用:
递归作为基础算法,应用非常广泛,比如在二分查找、快速排序、归并排序、树的遍历上都有使用递归
回溯算法、分治算法、动态规划中也大量使用递归算法实现
二分查找 (也叫做二分法):
概念:
二分查找(Binary Search)算法,也叫折半查找算法
当我们要从一个序列中查找一个元素的时候,二分查找是一种非常快速的查找算法
二分查找是针对有序数据集合的查找算法(后面会说明),如果是无序数据集合就遍历查找(比如数组遍历查询,或者链表遍历查询)

在这里插入图片描述

本质:
二分查找之所以快速,是因为它在匹配不成功的时候,每次都能排除剩余元素中一半的元素
因此可能包含目标元素的有效范围就收缩得很快,而不像顺序查找那样,每次仅能排除一个元素
通常情况下,二分法基本是操作有序集合(自带的数据结构,通常也称为集合,或者无论是线性表结构还是非线性表结构都可以称为集合)或者数组的,如果是无序,那么你二分法基本没有作用,因为二分法就是因为有序,从而可以导致排除一半元素的,而无序,不能判断哪边有,哪边没有,所以没有作用,如果使用,那么大概率会因为排错了而必然找不到,因为两边都有机会有正确结果,如果不是当前一边有结果,而是另外一边有结果,那么必然找不到了,所以总之,无序基本不会使用二分法,因为有错误风险(在另外一边必然找不到,使得明明存在确找不到的错误,如手动设置返回-1,就是找不到的意思),而基本使用遍历查找(比如数组遍历查询,或者链表遍历查询)
具体案例可以认为是我们生活中的猜数字游戏
小例子 :
在一个有序的数组中查找某个数字是否存在(操作下标即可,没有则返回-1,有则返回对应下标):
package com;

/**
 *
 */
public class test3 {
    public static int fa(int[] i, int j) {
        //定义头部下标,用来使得计算一半
        int head = 0;
        //定义尾部下标,用来使得计算一半
        //但是,要注意,这是大致的一半,因为在程序里,没有小数的下标,所以大多数是向下兼容其一半
        //比如3.5就是3,如果是根据数学上来说,头部和尾部都需要加上1才可,算起来平均加了1,但如果一方是0,那么也是相加相除,所以简单来说,要得到对应值的一半,必然是双方相加,除以2,就会得到一半,无论他代表什么意思,都是如此,无论从图像来看,还是中间相隔来看(不考虑两边),都是这样
        int last = i.length - 1;
        //定义初始值中间,用来使得计算一半,因为是操作下标的,所以结果也是下标(即可以是0)
        int h = (head + last) / 2;
        while (true) {
            if (i[h] == j) {

                return h;
            }
            //经过上面的等于判断,若还在这里,那么代表已经没有数据了,因为他们会排除自身(即+1和-1),所以基本必然会相同
            //放在前面运气好可以直接得到数据,就不用操作比较大小了
            if (head >= last) { //虽然基本上是==,但最好改成>=以防万一
                return -1;
            }
            //比较大小,然后进行靠拢
            if (i[h] > j) {
                last = h - 1;
                h = (head + last) / 2;
            } else if (i[h] < j) {
                head = h + 1;
                h = (head + last) / 2;
            }
        }


    }

    public static void main(String[] args) {
        int[] nums = {3, 12, 24, 31, 46, 48, 52, 66, 69, 79, 82};
        System.out.println(fa(nums, 66));

    }
}

//那么有个问题,为什么取一半比较好呢,这实际上是数学问题,因为取一半,成功并快速得到数据的概率要大,即一半是最稳定的(基本上不会出现某些极端,比如非常快找到,或者非常慢找到),所以我们取一半,因为程序就是需要稳定,而不能投机取巧,就相当于,你随便猜数字,你一直猜3或者取四分之三(排除少,这是主要的,因为无论你取四分之一还是四分之三,都是排除少的),运气好的话,一猜就中,运气不好就难猜中的,即依靠运气好的结果,所以成功并快速得到数据的概率要小,即在程序里可不看运气,而看稳定,或者说,成功的概率,所以我们取一半
至此,我们使用二分法查找数字操作完毕,接下来我们使用递归来操作
使用递归来操作:
package com;

/**
 *
 */
public class test4 {

    //递归实现二分法
    //实际上我们可以看对应的循环,发现每次都是改变两端的值,然后重新计算操作,所以我们可以这样
    public static int fb(int[] array, int i, int n, int var) {
        int h = (i + n) / 2;
        if (array[h] == var) {
            return h;
        }
        if (i >= n) {
            return -1;
        }
        //调用自身,使用自己的判断,就不用无限循环了
        if (array[h] > var) {
            //只要其中一个返回了,那么对应的结果都是一样的,而不会持续操作(所以时间复杂度与循环的基本差不多),这样,我们就将循环去掉了,以当前方法作为循环,这也是递归的思想(当然,最好不要有其他的操作,否则递归是不行的),即使用自己方法的判断,而不是使用循环里的判断,虽然他们基本是一样的
            return fb(array, i, h - 1, var);
        } else {
            return fb(array, h + 1, n, var);
        }

        //没有无限循环的情况下,需要保证可以返回,否则该方法会报错,你可以删掉这个返回就知道了,注意是无限循环哦,也就是while(true),当然不是无限循环也行,因为一般来说,只要是个循环,无论里面是否加上了return,外面也需要加上return,这是防止循环可能执行不到return的原因
        //当然,单纯的if也是,大多数通常是因为只使用if而没有使用else的问题


    }

    public static void main(String[] args) {
        int[] nums = {3, 12, 24, 31, 46, 48, 52, 66, 69, 79, 82};
        //参数传递:数组,两端值(0和nums.length - 1),以及需要查找的元素66
        System.out.println(fb(nums, 0, nums.length - 1, 66));
    }
}

/*
递归要素: 
递归结束条件:array[h] == var或者i == n
函数的功能:return h或者return -1,没有则返回-1,有则返回对应的下标
函数的等价关系式:
fb(array, i, n, var) = fb(array, i, h - 1, var)
或者
fb(array, i, n, var) = fb(array, h + 1, n, var)
*/


//我们可以发现,无论是循环还是递归,他们返回的都是下标,所以实际上他们都是完成这个下标获取功能

//这里需要再次的提醒一下递归的操作,实际上递归对比循环来说,除了是调用自身外,也可以不用向循环那样,因为不确定执行次数,使得操作无限循环,这里之所以这样说,在后面的二叉树的代码中有具体体现,当然递归在二叉树那里,操作非常友好,二叉树那里会说明的
经典案例:
一个有序数组有一个数出现1次,其他数出现2次,找出出现一次的数
比如:1 1 2 2 3 3 4 4 5 5 6 6 7 出现1次的数是7(这里考虑整体,总不能暴力操作一开始就取到7,就认为是O(n)吧)
如果我们使用暴力操作(即直接的循环数字来判断),那么就是O(n^2)
比如,拿出第一个,循环全部看看是否有第二个,基本是嵌套的循环,所以是O(n^2)
如果使用key和value的操作来解决,即hash来解决,由于同一个值的hash基本会相同的(因为引用是根据其变量来操作)
所以可以定义一个类数组(类里面有key和value),再次的定义一个变量kk用来保存只出现1的key,那么当他操作hash时,判断是否存在类
若不存在,可以先保存(类的key为下标,value为次数),将类的value值表示为1,再将kk的值设为该key或者该数组值(反正是一样的)
若存在,那么对应value值表示为2,如果kk的值与其key相同,则kk表示为空(看key什么类型了,如果是整数,那么表示0,如果是字符串或者引用,表示null,即以后表示的空,外面统称为默认值),否则不变
至此一个循环结束,kk的值就是外面要找的数,很明显,只需要一个循环即可,即O(n)
他之所以可以是O(n),主要是相同的值能够直接找到(hash),从而可以做好标记,比如直接确定下标,而暴力确需要嵌套,当然,数组也做标记也是O(n),只是需要他的下标是按照其值来的,否则找不到对应下标那么基本操作不了,即出现值不好针对下标那么基本操作不了,比如"5"和5,而在hash中,可以分开,但是如果出现哈希冲突,他可以使用链表,但操作链表的循环,必然比遍历数组循环要少或者等于(都进行冲突,都在一个链表),所以总体来说,如果是有相同的值的需求,或者有操作hash会相同的需求,一般使用哈希表,这样能够很快的根据标记(按照需求来)来解决其问题
那么可不可以再次的减低时间复杂度吗,可以,但是O(1)基本是不现实的(除非有特殊操作,比如之前的斐波那契数列的通项公式),那么自然需要O(logn)
思路:
使用二分查找(二分法,或者二分):
因为上面的数据是有序的,所以可以使用二分查找,而二分查找的时间复杂度就为 O(logn),为什么说二分查找是O(logn)呢,假设你有一个长度为8的数组,我持续的对他进行减半,最多需要3次,使得最后得到一个数,那么对于数学来说就是:2^n = 8,而我们需要这个n的次数,那么自然就是log8 ,而8一般代表我们的总数或者总节点数,对应数组来说是长度,也就是表示n,所以是logn=“我们执行的次数”,所以二分法其实基本就是O(logn),虽然是O(logn),但实际上是最多的查找,但多余的差别不大,可以省略(整体而言),一般都会少几次的,只是省略了而已,所以大多数的情况下,时间复杂度默认n是无限大的
前面也说明过了O(logn),在2^x = n那里,与这里大致是一样的(因为有多余,所以是大致),都去除了底数2,因为对时间复杂度来说,默认n是无限大的,所以时间复杂度是一样的(操作了省略)
接下来我们来实现该操作:
我们来分析上面的数,我们认为他是数组,很明显
偶数位索引跟后面的比相同,奇数位索引跟前面的比相同,则说明前面数据的都对(没有单个数),因为但凡其中"一个"的数在前面,就不会这样
或者偶数位索引跟前面的比相同,奇数位索引跟后面的比相同,则说明后面的都对(没有单个数)
上面的两种都是排除对的,因为我们需要其中错误的(单个),所以他们是两个判断,在程序里有具体说明
那么根据这样的思路,如果我们操作二分,如果其中一方是对的,那么将对的排除掉,然后继续二分,直到找到其中的值
即代码编写如下:
package com;

/**
 *
 */
public class test5 {
    public static int fc(int[] array) {
        int head = 0;

        int last = array.length - 1;
        int h = (head + last) / 2;
        while (true) {
            //代表偶数位


            if (h % 2 == 0) {
                //这里需要判断,防止当前第0个下标和最后一个下标,因为如果到了第0个下标或者最后一个下标,那么自然他必定是指定数,且由于h-1或者h+1的存在会使得边界报错,所以需要判断
                if (h == 0 || h == array.length - 1) {
                    return h;
                }
                //代表前面的数据都是对的,将他排除
                if (array[h] == array[h + 1]) {
                    //进行排除
                    head = h + 2; //既然h+1与他一样,就没有必要在操作里面了
                    h = (head + last) / 2;
                    //代表后面的是对的,将他排除
                } else if (array[h] == array[h - 1]) {
                    //进行排除
                    last = h - 2; //与上面同理
                    h = (head + last) / 2;
                } else {
                    //如果都没有,那么必然就是当前的一个数,因为只有一个数是无论如何都不会相等的(即既不等于前,又不等于后)
                    return h;
                }


                //代表奇数位
            } else {
                //代表前面的数据都是对的,将他排除
                if (array[h] == array[h - 1]) {
                    //进行排除
                    head = h + 1;
                    h = (head + last) / 2;
                    //代表后面的数据都是对的,将他排除
                } else if (array[h] == array[h + 1]) {
                    last = h - 1;
                    h = (head + last) / 2;
                } else {

                    //如果都没有,那么必然就是当前的一个数,因为只有一个数是无论如何都不会相等的(即既不等于前,又不等于后)
                    return h;


                }

            }
            
            //上面就是这样的思路:
            /*
            偶数位索引跟后面的比相同,奇数位索引跟前面的比相同,则说明前面数据的都对(没有单个数),因为但凡其中"一个"的数在前面,就不会这样
            对应于偶数和奇数的第一个判断
		   或者
		   偶数位索引跟前面的比相同,奇数位索引跟后面的比相同,则说明后面的都对(没有单个数)
		   对应于偶数和奇数的第二个判断
*/

            //若都没有,则直接退出,因为前面是加2的,所以要考虑大于的情况,等于号,上面的判断下标就解决了
            //因为由于加2的原因,所以必然在没有值时(没有单个),才基本会出现head大于last的情况,而只有单数个才不会
            //所以这里不用加等于,否则可能最后一个单个数也会返回-1
            if (head > last) {
                return -1;
            }


        }


    }

    //如果可以修改,你可以试着修改一下
    public static void main(String[] args) {
        int[] array = {1, 2, 2, 3, 3, 4, 4, 7, 7, 6, 6};
        System.out.println(fc(array));
    }

    //实际上,他是操作有序的,而无序的那么对应的思路是没有用的,比如你将6分开,一个放前面一个放后面,那么前面一个6会认为是错误的(单个),导致思路错误,所以需要有序
}


//虽然他是循环,但是他这个循环的次数确根据log来操作的,所以他就是O(logn),即并不是循环就一定是O(n),所以在前面也说明过,我们通常只会看循环或者说(及其)变化大(通常是最大,主要是这个)的代码来进行估算(时间复杂度)
//即主要看具体变化,所以如果while(true)里面是根据n来的,那么自然就是O(n),即看具体次数(变化),而不是他写上什么次,就一定是什么时间复杂度,如可能里面也有循环,比如O(n^2)
时间复杂度:
时间复杂度就是 O(logn),以方法为主的意思
优缺点
优点:速度快,不占空间,因为已经固定了,即不开辟新空间
缺点:必须是有序的数组,数据量太小没有意义,但数据量也不能太大,因为数组要占用连续的空间
且上面的代码,基本只能解决2个中找1个,因为我们就是根据这个需求来编写的,如果是其他的操作,可能会出现问题,比如是三个相同的,且只要单个就行(只要不影响他们的顺序,即1,1,6,2,2,这个6还是可以得到,1,1顺序没有影响),但是代码也的确解决的对应的问题,虽然他有扩展(上面的6),但只要解决即可,就如一个公式,可以解决很多问题(可能包括对应问题的扩展),而不只是该一个,所以具体问题具体分析吧
应用 :
有序的查找都可以使用二分法
说到应用,我们可以通过前面的学习可以发现,实际上对应的算法可以认为是数学知识,而用数学解决的题就是应用
所以对应的知识(算法)基本是操作具体的题的,当然,基础知识(数组,链表)基本贯彻所有题(应用)
所以学习算法就相当于是学习数学知识,那么必然是需要大量的练习(做题)才可熟练的,就如学习数学一样
具体应用(二分法):
比如如何快速定位出一个 IP 地址的归属地?
如果 IP 区间与归属地的对应关系不经常更新,我们可以先预处理这 12 万条数据,让其按照起始 IP 从 小到大排序
如何来排序呢?
我们知道,IP 地址可以转化为 32 位的整型数
所以,我们可以将起始地 址,按照对应的整型值的大小关系,从小到大进行排序
当我们要查询某个 IP 归属地时,我们可以先通过二分查找,找到最后一个起始 IP 小于等于这个 IP 的IP 区间
然后,检查这个 IP 是否在这个 IP 区间内,如果在,我们就取出对应的归属地显示,如果不在,就返回未查找到
数据结构与算法高级 :
树:
树的概念:
有很多数据的逻辑关系并不是线性关系,在实际场景中,常常存在着一对多,甚至是多对多的情况,而不是单对单(如数组下标对应一个数,一个key对应value,而他们这样的显示,正好是线性表,没有横向扩展)
家谱(你是小明):

在这里插入图片描述

组织结构:

在这里插入图片描述

书的目录(中间没有连,省略了):

在这里插入图片描述

以上的数据结构,我们称为树
在数据结构中,树的定义如下:
树(tree)是n(n≥0)个节点的有限集,当n=0时,称为空树,在任意一个非空树中,有如下特点:
有且仅有一个特定的称为根的节点,当n>1时,其余节点可分为m(m>0)个互不相交的有限集
每一个集合本身又是一个树,并称为根的子树
一个标准的树结构:

在这里插入图片描述

节点1是根节点(root),没有父节点
节点5、6、7、8,9是树的末端,没有"孩子",被称为叶子节点(leaf:叶) ,一般子节点就代表末尾
节点2、3、4是树的中端,有父节点,有孩子,被称为中间节点或枝节点
上面的根节点(没有对应的父节点),中间节点(枝节点),叶子节点(对应子节点,或者叶节点,子节点因为是节点的子,所以无论是其他节点,还是叶子节点,都可以称为对应子节点或者就是子节点,如果是后一种意思就是叶子节点了,而前一种,一般需要结合其他节点或者一些语录来说明,比如,结尾节点,或者结尾子节点,“结尾"就是语录,或者其子节点,或者该节点的子节点,那么"其”,"该节点"就是其他节点,等等,才可对应),或者相对来说的父节点,他们都可以统称为节点
图中的虚线部分,是根节点1的其中一个子树
树的最大层级数,被称为树的高度或深度,上图这个树的高度是4(一般说明的是层),举例:

在这里插入图片描述

树的分类如下:

在这里插入图片描述

树的种类非常多,我们会选几个有代表性的详细讲解
二叉树:
二叉树(binary tree)是树的一种特殊形式,二叉,顾名思义,这种树的每个节点最多有2个孩子节 点
注意,这里是最多有2个,也可能只有1个,或者没有孩子节点

在这里插入图片描述

上图中,6实际上是右孩子节点(因为他指向右边,在程序里,就代表右边的变量,而不是左边的变量,以后就知道了)
二叉树节点的两个孩子节点,一个被称为左孩子(left child),一个被称为右孩子(right child)
这两个孩子节点的顺序是固定的,一般是左孩子小于右孩子
满二叉树:
一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树
简单来说,就是除了最后一层无任何子节点外,每一层上的所有结点都有两个子节点的二叉树

在这里插入图片描述

完全二叉树:
对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n
如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树

在这里插入图片描述

满二叉树要求所有分支都是满的,而完全二叉树只需保证最后一个节点之前的节点都齐全即可,所以如果上面的12的位置,变成右边,那么就不是完全二叉树了,所以没有进行跳位置(原来左边变成右边)就是完全二叉树,而正是没有跳位置,那么就相当于最后一个节点之前的节点于满二叉树是一样的(如果是右边,那么就少了左边,不是完全二叉树,而是左边,那么就是一样的,是完全二叉树)
实际上一个数,并不是非要是固定顺序,如果上面的最后一个节点,在右边,那么也是可行的,而不是顺序必须是在左边,这样如果是右边,那么也不是完全二叉树了,所以实际上完全二叉树代表:顺序与满二叉树相同(即从上至下、从左到右的顺序,中间不能空缺)
格外注意:这里说明的是位置,而不是数字,数字只是用来保存的,无论是左节点还是右节点,只需要按照从上到下,从左至右的保存内容即可,上和下,左和右,并不是相对于左右节点的,即并非要左右节点都存在,可以只存在一个,或者都不存在,比如8和12,就是从左到右,中间的都可以没有,比如后面的数字存储的图片,或者二叉查找树,都满足上和下,左和右的保存,但一般的二叉树的节点数值(这里是数值了)是小的在左,大的在右(如后面的二叉查找树),除非有其他特殊的二叉树,所以实际上一般如何保存,就看对应二叉树是什么意思了,因为二叉树有多种,所以并非一定是小在左,大在右的规律(针对节点数值)
二叉树的存储:
二叉树属于逻辑结构,因为是我们要这样操作的,可以使用链表和数组等物理结构来进行存储
链式存储:

在这里插入图片描述

二叉树的每一个节点包含3部分:
存储数据的data变量
指向左孩子的left指针
指向右孩子的right指针
数组存储:
使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上
如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来

在这里插入图片描述

寻址方式:
一个父节点的下标是n,那么它的左孩子节点下标就是2* n+1、右孩子节点下标就是2* (n+1),当然,由于不好操作父节点,所以一般我们都会从1开始,在后面的二叉堆中会具体说明的
而如果不是下标,那么一般就是2*n以及2 *n+1,所以说,实际上数字是同样的操作,只是由于下标的原因,所以要加1(一个公式到处使用,但有时也需要根据实际情况来加减,因为有些公式可能只是针对该一种起始数字情况来表示的,比如这里就是针对0开始的情况,因为对应的该公式就是围绕下标来的,那么自然受起始数字情况的影响)
对于一个稀疏的二叉树(孩子不满)来说,用数组表示法是非常浪费空间的(有空格,比如上面的下标5,如果是完全二叉树或者满二叉树那么可以使用数组表示)
但总体来说,二叉树一般用链表存储实现(二叉堆除外),但是如果已经确定了,那么可以操作数组存储(因为确定了,基本不会增加节点了,但还是建议使用链表,因为数组可能有空的,比如不是满二叉树和完全二叉树,这就要考虑很多情况了,比如在不是满二叉树和完全二叉树的情况下,建议使用链表,除非急切需要数组的查找,在是满二叉树和完全二叉树的情况下,可以使用链表和数组,这时就看你需要数组和链表的哪个优点了,这里表示不是急切的,如果是急切的,那么选择急切的那一个,所以二叉堆就利用数组的好查找的优点,基本是急切的,后面会说明的)
综上所述:使用链表存储实现多因为是满二叉树和完全二叉树的情况下机会(相对的优点)平等(都是中等和急切),而不是满二叉树和完全二叉树的情况下,链表大于数组,因为数组必需要急切(而不能是中等,因为中等相对来说没有优点,但也可以使用),而链表可以急切或者中等
二叉查找树(也可以称为二叉树,因为他属于二叉树的操作,只是有条件):
二叉查找树(binary search tree),二叉查找树在二叉树的基础上增加了以下几个条件(也就是之前说的大多数二叉树的意思):
如果左子树不为空,则左子树上所有节点的值均小于根节点的值
如果右子树不为空,则右子树上所有节点的值均大于根节点的值
左、右子树也都是二叉查找树,我们一般也称二叉查找树是有序的
如下图:

在这里插入图片描述

二叉查找树要求左子树小于父节点,右子树大于父节点,正是这样保证了二叉树的有序性
因此二叉查找树还有另一个名字:二叉排序树(binary sort tree)
我们也可以发现,实际上他可以认为是从下到上,从左到右的依次递增,只是通常我们只能通过根节点来完成具体操作,因为根节点是保存所有信息的,除非他们是双向的(除了左右的连接外,还有头连接,左右连接统称为尾连接),但一般根节点排除的最多,所以都会使用根节点(因为大多数来说,只会存在一方)
我们也可以发现,二叉查找树,与二分法有点类似,都是进行排除一方的
查找:
例如查找值为4的节点,步骤如下:
1:访问根节点6,发现4<6
2:访问节点6的左孩子节点3,发现4>3
3:访问节点3的右孩子节点4,发现4=4,这正是要查找的节点
我们的确可以发现,与二分法基本类似,只是,他首先给出了具体的区间范围,而不用我们二分法自行的划分范围了
即我们mysql中索引的创建,实际上也就是来进行区分范围,或者说,建立树,使得容易查找,但是建立树需要时间

在这里插入图片描述

对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就 是O(logn),即类似于二分法
和树的深度是一样的,这种方式正是二分查找思想,而之所以类似于二分法,是因为一般根节点或者中间节点是取对应数的中间的数的,即看起来对应的左节点对应前面,而右节点对应后面,所以类似
这里一般会保存最大数和最小数来决定根节点的,而正是因为有前面和后面,所以是横向的扩展,即是非线性表了
插入:
例如插入新元素5,步骤如下:
1:访问根节点6,发现5<6
2:访问节点6的左孩子节点3,发现5>3
3:访问节点3的右孩子节点4,发现5>4
4:5最终会插入到节点4的右孩子位置

在这里插入图片描述

二叉树的遍历(这里以二叉查找树为主) :
二叉树,是典型的非线性数据结构,遍历时需要把非线性关联的节点转化成一个线性的序列
以不同的 方式来遍历,遍历出的序列顺序也不同
二叉树的遍历包括:
1:深度优先遍历
所谓深度优先,顾名思义,就是偏向于纵深,"一头扎到底"的访问方式
它包括:
前序遍历:
二叉树的前序遍历,输出顺序是根节点、左子树、右子树
注意:下面的是代表节点,而不是具体数,所以5的这个节点的数值是没有大于1这个节点的数值的(因为这里操作的是二叉查找树,我们一般虽然也会称为二叉树),在后面更新操作中会说明,除非你的二叉树是特别的,否则一般都是这样认为,但一般不会,否则会使得查找不到,更新操作那里有说明

在这里插入图片描述

步骤如下:
1:首先输出的是根节点1
2:由于根节点1存在左孩子,输出左孩子节点2
3:由于节点2也存在左孩子,输出左孩子节点4
4:节点4既没有左孩子,也没有右孩子,那么回到节点2,输出节点2的右孩子节点5
5:节点5既没有左孩子,也没有右孩子,那么回到节点1,输出节点1的右孩子节点3
6:节点3没有左孩子,但是有右孩子,因此输出节点3的右孩子节点6
简单来说,前序遍历就是靠左遍历,即先将左边的都进行遍历,然后才根据子树一路找右节点,但需要解决其一边的所以树(子树)才可(一般是左边),由于是先输出根,所以称为前序遍历
到此为止,所有的节点都遍历输出完毕
中序遍历:
二叉树的中序遍历,输出顺序是左子树、根节点、右子树

在这里插入图片描述

步骤如下:
1:首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去
一直找到不 再有左孩子 的节点,并输出该节点,显然,第一个没有左孩子的节点是节点4
2:依照中序遍历的次序,接下来输出节点4的父节点2
3:再输出节点2的右孩子节点5
4:以节点2为根的左子树已经输出完毕,这时再输出整个二叉树的根节点1
5:由于节点3没有左孩子,所以直接输出根节点1的右孩子节点3
6:最后输出节点3的右孩子节点6
简单来说,就是先输出左来遍历,然后输出父节点,然后就是父节点的右,但需要解决其一边的所以树(子树)才可(一般是左边)
由于先输出根里面的,即中序遍历(而不是中间的意思,而是根节点在中间输出)
到此为止,所有的节点都遍历输出完毕
后序遍历:
二叉树的后序遍历,输出顺序是左子树、右子树、根节点

在这里插入图片描述

步骤如下:
1:首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去
一直找到不 再有左孩子 的节点,并输出该节点,显然,第一个没有左孩子的节点是节点4
2:输出右节点5
3:输出节点4的父节点2
4:以节点2为根的左子树已经输出完毕,这时再输出整个二叉树的右子树
即访问根节点的右孩子,如果这个右孩子拥有左孩子,则继续深入访问下去
一直找到不再有左 孩子 的节点,如果没有左孩子则找右孩子,并输出该节点6
5:输出节点6的父节点3
6:输出根节点1
简单来说,首先输出左边的,然后输出右边的,最后才是父节点,但需要解决其一边的所以树(子树)才可(一般是左边)
由于最后输出根节点,所以称为后序遍历
到此为止,所有的节点都遍历输出完毕
总结:
这三种遍历是我们需要这样操作的遍历,就如数学题要求的条件一样,当然,若你觉得麻烦,可以不这样操作
所以后面还是广度优先遍历,但是他的这个遍历需要的代码在一定的程度上要少很多,看后面的递归操作就明白了
前序遍历:先输出根节点,然后树根据父,左,右一路往下输出(首先是左边,然后是右边),如果该节点是父,那么继续左(先输出了父),右
中序遍历:后输出根节点,中间输出的,然后树根据以左,父,右一路往上输出,必须从左开始,即先输出父的左边,如果该节点是父,那么继续找左节点(没有先输出父),然后输出父和右,当右输出完,或者没有右,才继续去父的向上走,首先是左边,当输出根节点后,最后右边
后序遍历:后输出根节点,然后树根据左,右,父一路往上输出,必须从左开始,然后才是右,最后是父,如果该节点是父,那么继续找左节点(没有先输出父),当最后输出父后继续往上走
很明显,对应的前序,中序,后序,就是父的前输出(父,左,右),中间输出(左,父,右),后输出(左,右,父)
你可以将对应的节点,默念上面的顺序,即每个节点都要满足(这句话是关键),所以才会出现上面说的"如果该节点是父"
但都是先左后右(这是规定的,因为一般是小的数放在左边,二叉树也一般统一这样输出的),那么我们根据这样的顺序,可以很容易的理解这三个遍历了,而正是因为前序遍历是父在前,所以向下输出,其他的都是向上输出,因为先输出底层的(子),而不是先输出上层的(父)
当然,你也可以自己编写一个遍历方法,而不用操作他规定的遍历,就如前面我操作的反过来查询所有节点的方法一样,操作自己的遍历
为了实现上面的查找,插入,和三个遍历,我们来编写代码:
package com;

import f.Tree1;

/**
 *
 */
public class Tree {
    //定义头部节点
    TreeNode root;
    //总共有多少个节点
    int max;


    //打印的字符串
    String array = "";


    //定义节点信息,我们使用链表法来操作,因为链表整体上比数组好
    public class TreeNode {

        //定义本身的值
        int data;
        //定义左节点
        TreeNode left;
        TreeNode right;

        public TreeNode(int data) {
            this.data = data;
        }
    }


    //操作插入节点,指定根节点和插入的数据
    public TreeNode insert(TreeNode treeNode, int data) {
        //如果没有节点,那么直接赋值即可,无论是操作根节点,还是对应的左节点和右节点
        if (treeNode == null) {
            treeNode = new TreeNode(data);
            max++;
            return treeNode;
        }
        //插左边
        if (data < treeNode.data) {
            //再次的调用自身,传递当前的左节点和要添加的数据,让当前的左节点继续使用自身的操作来进一步判断,就如我们传递根节点一样的进行赋值
            treeNode.left = insert(treeNode.left, data);
        }
        //插右边
        else if (data > treeNode.data) {
            //再次的调用自身,传递当前的右节点和要添加的数据,让当前的右节点继续使用自身的操作来进一步判断,就如我们传递根节点一样的进行赋值
            treeNode.right = insert(treeNode.right, data);
        } else {
            //无论是操作左边还是右边,只要他们有一个是相同的,进行覆盖赋值
            treeNode.data = data;
        }
        //返回其得到的(上面创建的节点)
        return treeNode;

    }


    //前序遍历
    public void preSelect(TreeNode treeNode) {
        if (treeNode == null) {
            return;
        }
        array = array + treeNode.data + ",";
        //他会一路找过去,直到被返回
        preSelect(treeNode.left);
        //只有左边找完了,才会操作右边,很明显左边先操作
        preSelect(treeNode.right);

    }

    //中序遍历
    public void inSelect(TreeNode treeNode) {
        if (treeNode == null) {
            return;
        }
        inSelect(treeNode.left);
        //代表父的输出位置
        array = array + treeNode.data + ",";
        inSelect(treeNode.right);

    }

    //后序遍历
    public void latSelect(TreeNode treeNode) {
        if (treeNode == null) {
            return;
        }
        latSelect(treeNode.left);
        latSelect(treeNode.right);
        array = array + treeNode.data + ",";
    }

    //打印信息,需要在操作遍历后打印,而不要都遍历打印,会叠加的
    public void Select() {
        if (array != "") {
            String a = "";
            String[] split = array.split(",");
            System.out.print("[");
            for (int i = 0; i < split.length; i++) {
                if (i == split.length - 1) {
                    a = a + split[i] + "]";
                } else {
                    a = a + split[i] + ",";
                }
            }
            System.out.println(a);
        } else {
            System.out.println("[]");
        }
        array = "";
    }

    //查找节点
    public TreeNode getNode(int data) {

        TreeNode treeNode = root;
        if (treeNode == null) {
            System.out.println("没有要查找的节点");
            return null;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要查找的节点");
                return null;
            }
            if (treeNode.data > data) {
                treeNode = treeNode.left;
            } else if (treeNode.data < data) {
                treeNode = treeNode.right;
            } else if (treeNode.data == data) {
                return treeNode;
            }
        }
    }

    //删除节点
    public void delete(int data) {
        TreeNode treeNode = root;
        TreeNode temp1 = null;
        if (treeNode == null) {
            System.out.println("没有要删除的节点");
            return;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要删除的节点");
                return;
            } else if (treeNode.data > data) {
                temp1 = treeNode;
                treeNode = treeNode.left;
            } else if (treeNode.data < data) {
                temp1 = treeNode;
                treeNode = treeNode.right;
            } else if (treeNode.data == data) {
                if (temp1.left == treeNode) {
                    temp1.left = null;
                    return;
                }
                if (temp1.right == treeNode) {
                    temp1.right = null;
                    return;
                }
            }
        }
    }

    //修改节点,由于二叉树是从左到右的,并且这里是小的在左,大的在右的规则,那么我们也要遵守这个规则,否则不遵守的话,其他的操作,比如查找是必然找不到的,我们看如下解释:
    //当然,只需要满足自己的父节点即可,其他的节点是大的也好,还是小的也好,都不用考虑,但这里也要注意,他自身不能超过他的父节点(包含父节点的父节点)中的一个有限范围
    //比如:
    /*
        9           9
      4                7
    1   5            8   6
    上面的5不能大于等于9,上面8不能小于等于9,这个9称为上一个拐点(这里是第一个,因为只有一个,但实际上是上一个拐点),以此内推,也就是,我们修改的值若在右边,需要大于父节点,并小于上一个拐点,如果是在左边,那么小于父节点,并大于上一个拐点
    如果是这样:
    			9
    		7	    14
    		      10  15
    		        11
    		        对于11来说,他的上一个拐点是14,所以上一个拐点是相对于当前节点来说的
    		        
    		        当然如果5大于等于9,那么我们通过二分法必然是不会通过4的(到右节点了,或者就是本身),即永远找不到其里面的对应值了,这是大多数二叉树的特性,就如我们猜数字一样,必须在一个区间里面,而不能跳出,因为数字就是这样
    */
    public void update(int data, int da) {
        TreeNode treeNode = root;
        TreeNode temp1 = root;
        TreeNode temp2 = root;
        //定义拐点,只需要存上一个拐点即可
        TreeNode aa = null;

        if (treeNode == null) {
            System.out.println("没有要修改的节点");
            return;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要修改的节点");
                return;
            } else if (treeNode.data > data) {
                temp2 = temp1;
                temp1 = treeNode;
                treeNode = treeNode.left;
                //拐点
                if (temp2.right == temp1 && temp1.left == treeNode) {
                    aa = temp2;
                }
            } else if (treeNode.data < data) {
                temp2 = temp1;
                temp1 = treeNode;
                treeNode = treeNode.right;
                //拐点
                if (temp2.left == temp1 && temp1.right == treeNode) {
                    aa = temp2;
                }

            } else if (treeNode.data == data) {

                TreeNode temp3 = treeNode.left;
                TreeNode temp4 = treeNode.right;
                int max;
                int min;
                if (temp3 != null) {
                    while (true) {
                        if (temp3.right == null) {
                            min = temp3.data;
                            break;
                        }
                        temp3 = temp3.right;
                    }
                } else {
                    min = treeNode.data;
                }
                if (temp4 != null) {
                    while (true) {
                        if (temp4.left == null) {
                            max = temp4.data;
                            break;
                        }
                        temp4 = temp4.left;
                    }
                } else {

                    max = treeNode.data;
                }
                if (min == treeNode.data && max == treeNode.data) {
                    if (treeNode == root) {
                        treeNode.data = da;

                        return;
                    }
                    //这里需要上面的条件,所以最好使用else,因为如果上面的没有判断,那么必然有一个不相等,如果不是else,那么可能是相等的,也会执行到这里,因为不是root即可
                    //这样可能使得,明明节点正确,但是数值不合理,因为你不能大于本身了,你可以操作12修改12,去掉这个else就知道了,将去掉和不去掉的结构对比,去掉的出现不合理,而没有去掉的没有出现
                    //即实际上若要使用if,而不使用else,这里需要加上max != treeNode.data,这样就能去掉else,同样的,后面的if若要去掉else,也要加上min != treeNode.data,因为else存在使得我不会操作时,但你会操作
                    //当然,他们的else后面的if也能这样操作,但是没有必要(加上也行,反正基本是同样的结果),因为第一个if已经存在条件了,这里考虑的是去掉else的情况
                } else if (min == treeNode.data) {
                    if (da >= max) {


                        System.out.println("修改数值不合理");
                        return;
                    }
                } else if (max == treeNode.data) {
                    if (da <= min) {


                        System.out.println("修改数值不合理");
                        return;
                    }
                }
                //上面能够解决自身的左节点和右节点的取值范围


                //操作拐点(如果没有,那么自然不会操作到)
                if (temp1.left == treeNode) {
                    if (aa == null) {
                        if (da >= temp1.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    } else {
                        if (da >= temp1.data || da <= aa.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    }
                    return;
                }
                if (temp1.right == treeNode) {
                    if (aa == null) {
                        if (da <= temp1.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    } else {
                        if (da <= temp1.data || da >= aa.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    }
                    return;
                }


            }
        }
    }

    public static void main(String[] args) {
        Tree tree = new Tree();
        tree.root = tree.insert(tree.root, 10);
        tree.root = tree.insert(tree.root, 8);
        tree.root = tree.insert(tree.root, 11);
        tree.root = tree.insert(tree.root, 7);
        tree.root = tree.insert(tree.root, 9);
        tree.root = tree.insert(tree.root, 12);
        /*
                    10
                8       11
              7    9        12
              前序遍历是:10,8,7,9,11,12
              中序遍历是:7,8,9,10,11,12
              后序遍历是:7,9,8,12,11,10
         */
        System.out.println("=============前序遍历是:=============");
        tree.preSelect(tree.root);
        tree.Select();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect(tree.root);
        tree.Select();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect(tree.root);
        tree.Select();
        System.out.println("总个数是:" + tree.max);

        //查找数值为7的节点
        TreeNode node = tree.getNode(7); //若返回null,代表没有该节点
        if (node != null) {
            System.out.println("数值为:" + node.data);
        } else {
            System.out.println("没有节点,也就没有数值");
        }
        //删除数值为7的节点
        tree.delete(7);
        System.out.println("=============前序遍历是:=============");
        tree.preSelect(tree.root);
        tree.Select();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect(tree.root);
        tree.Select();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect(tree.root);
        tree.Select();

        //修改数值为7的节点,这里可以自己测试,如果有问题,可以试着修改,我测试时,基本没有问题
        tree.update(8, 9);
        System.out.println("=============前序遍历是:=============");
        tree.preSelect(tree.root);
        tree.Select();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect(tree.root);
        tree.Select();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect(tree.root);
        tree.Select();
    }
}

我们可以发现,上面基本上都是使用递归的方法来进行操作的,那么有个问题,可不可以使用循环来操作,在前面我说过,大多数(基本上都可以,只要是多次操作的就能使用循环)操作都可以使用循环来操作,这里也不例外,具体循环操作如下:
package f;

import com.Tree;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 *
 */
public class Tree1 {
    //定义头部节点
    TreeNode root;
    //总共有多少个节点
    int max;

    //定义节点信息,我们使用链表法来操作,因为链表整体上比数组好
    public class TreeNode {

        //定义本身的值
        int data;
        //定义左节点
        TreeNode left;
        TreeNode right;

        public TreeNode(int data) {
            this.data = data;
        }
    }


    //操作插入节点,指定插入的数据,因为是循环,所以我们直接的操作root变量
    public void insert(int data) {
        //如果没有数据,那么直接创建
        if (root == null) {
            root = new TreeNode(data);
            max++;
            return;
        }
        TreeNode temp1 = root;
        while (true) {
            //插入左边
            if (data < temp1.data) {
                if (temp1.left == null) {
                    temp1.left = new TreeNode(data);
                    max++;
                    return;
                } else {
                    temp1 = temp1.left;
                }

            }
            //插入右边
            if (data > temp1.data) {
                if (temp1.right == null) {
                    temp1.right = new TreeNode(data);
                    max++;
                    return;
                } else {
                    temp1 = temp1.right;
                }

            }
        }
    }
    //我们可以发现,使用一个循环即可


    //那么我们来分析一下如何遍历,在之前,我们是使用递归执行当前方法,实际上就是都进行判断是否为null,来决定往后走
    //所以导致在前面的节点可以操作后面的节点打印的
    //这里同样也是如此,我们也需要判断是否为null,但是循环有个问题,递归可以操作回退
    //但循环一般并不会操作回退(回退:当前方法再次执行不会影响当前,即相当于一个替身往前执行,且互不影响,替身执行时,我们需要等待,导致其可以始终回到上一个节点或者地方,也就是递归的方法调用方法)
    //所以方法有回退的操作,但是在一个方法里面的代码确没有回退的属性,那么如何操作呢,我们先解释如下:
    //这里也证明,递归除了可以操作利用当前方法外,也可以通过方法自身的格外属性来进一步操作(如回退,因为返回,实际上递归也包含该作用)
    //这个回退导致,他执行完不会影响其他方法的执行,确需要等他他们执行完,这也是方法或者说代码本来的作用,只是在递归里具体实现了,使得递归可以完全的非常简便的操作三个遍历
    //即使得操作了后一个节点,我们也可以看到他操作了两个调用方法,每个调用方法都有两个调用方法,使得他完全的按照对应遍历的操作执行,使得每个节点都进行了操作,而正是因为这样,所以导致对应的输出位置就是代码三种遍历模式
    //即在前面,调用前就操作了输出,在中间,只有左先调用才能输出,在后面,只能他们都调用才能输出,所以很明显,对应的两个调用方法,代表确定了左和右,而左在前,那么必然先确定左
    //我们仔细的观察前序遍历,不难发现,他就是从根节点开始,一路往左边慢慢移动,当左边没有节点后,就返回头节点(递归的原因,他们嵌套调用),然后再操作右边,然后继续回到上个节点,并且可以发现,左和右是一起的,使得若打印在他们前,那么就是父,左,右,因为左边直接到后边了
    //如果是中间(中序遍历),那么就是先打印左的,然后才是父,然后是右,如果是后面(后序遍历),那么就是左,右,父
    //简单来说就是:当前节点,判断是否有左节点和右节点,而打印就是看何时的出来,即到当前节点(对应其两个方法的参数来说就是父节点),这里理解了那么他的执行流程就理解了
    /*
    因为他就是左直接到右,而我们输出的是当前节点,这里用言语是很难说明的,看自己的理解吧
    //这一行的打印就是父在前
    preSelect(treeNode.left); 
    //这一行的打印就是左边操作完后的打印,即父在中间
    preSelect(treeNode.right); 
    //这一行的打印都操作完后的打印,即父在最后
     */
    //而循环在当前执行后会进行影响,因为他基本只能是操作赋值的作用
    //也就是说,我们要通过循环来实现回退的效果,那么循环可以实现回退吗,基本不能,这也是在前面说的循环虽然能够解决大多数问题,但是有些可能解决不了的原因
    //你可以认为,循环的算法是基本知识,而解决高级的就需要其他算法了,当然,可能在以后,可以用循环解决
    //虽然循环不能解决回退,但并不是不能解决前序遍历,中序遍历,后序遍历,后面会给出代码
    //好了,现在我们来进行操作

    //这三种遍历,如何不用回退来操作呢,我们先进行分析,我们主要的任务是获得上个节点,因为回退可以得到(上一个变量是已经操作赋值的,每次是其中一个节点),那么我们也需要得到,只要得到了上个节点,我们就能完成这个需求
    //但是由于单纯的循环,得到上个节点非常困难,因为需要定义非常多的变量,在未知长度下,基本是不能操作的,所以我们应该是要考虑如何得到上一个节点
    //得到上一个节点,必然需要他是存在的,就如回退一样,上一个方法的变量内容是上一个节点的,即存在的,很明显,我们可以选择存放节点
    //即我们以前序遍历为例子,我们将所有的左边节点(包括根节点)都存放好后(从左边开始),然后反过来的操作他们的右边节点(因为从左边开始的),然后继续存放右边节点的左边节点(左边开始),在存放中,可以选择打印或者先保存再打印都行
    //这样我们就能解决了,很明显,我们需要栈来存放,因为他是先进后出的,因为要反过来操作,而这样的理论很明显,与回退基本类似

    //根据上面的思路,很明显,我们再存放好所有的左边节点后(因为是从左开始的),根据出栈的左边节点,找到右边节点,然后继续操作该右边节点存放好的所有的左边节点
    //当然,若没有左边节点了,那么就直接操作右节点了,如果右节点也没有,那么该节点出栈,拿上一个节点继续操作(自然就是操作右边,因为我相对于上一个就是他的左边),这样就与回退的操作是差不多的了,也就是与两个调用方法类似了
    //只不过他那里是利用方法的等待,而这里是利用栈来解决,而函数调用也是使用栈的,即他们还是有具体联系的,即出栈和方法调用完毕相关

    //前序遍历
    public void preSelect() {


        int[] in = new int[max]; //定义存放的值
        int i = 0;
        //当成栈来使用
        TreeNode[] a = new TreeNode[max]; //设置max是防止数组越界
        int aa = 0; //定义边界下标

        TreeNode treeNode = root;
        //由于次数不固定,所以需要无限循环,因为我们可以随时的添加,但实际上他也可以操作固定规律的或者某些条件的,只要你计算得到一方的执行次数即可
        //这里我就要提一下了,在循环中,我们尽量不要操作无限循环,因为如果在项目中出现问题,那么是非常的严重的,最好找到规律执行有限次数或者操作某些条件,如果实在没有规律或者条件,那么操作无限循环的话
        //最好尽量每个判断都有结尾的break
        if (treeNode == null) {
            System.out.println("[]");
            return;
        }

        //这里需要两个循环,外面的循环是保证回退,里面的循环是保证存放左节点
        while (true) {
            //如果他的根节点是null,那么直接的退出,但这里可能也会判断是否有右节点,导致会进行退出,所以这里还需要加上只有栈有值,就不执行
            if (treeNode == null && aa == 0) {
                break;
            }
            while (true) {
                //如果没有左边节点或者右边节点,那么跳出循环
                if (treeNode == null) {
                    break;
                }
                //保存当前节点的值
                in[i] = treeNode.data; //这个可以认为是放在前面的打印(只是这里我们首先保存而已,然后来实现自己操作的输出,看后面的代码就知道了)
                i++;
                //存放左边节点(包括根节点)
                a[aa] = treeNode;
                aa++;
                //到另外一个左边节点(从左边开始,因为左在右的前面,这是遍历的规则)
                treeNode = treeNode.left;
                //这个循环可以认为是preSelect(treeNode.left);
                //而循环外面的操作就是preSelect(treeNode.right);

            }
            //上面的循环已经得到了当前根树的所有左边节点了,很明显,根据上面的分析,然后要看他是否有右节点
            //拿取外面的左边节点,栈的第一个,也就是最后一个左边节点
            treeNode = a[--aa].right;
            a[aa] = null; //既然是出栈,就要为null(即默认值)

        }
        //外面可以发现,他每次都会一路获取过去的,与回退是一样的了


        String b = "";
        System.out.print("[");
        for (int ii = 0; ii < in.length; ii++) {
            if (ii == in.length - 1) {
                b = b + in[ii] + "]";

            } else {
                b = b + in[ii] + ",";
            }
        }
        System.out.println(b);


    }

    //以此类推,实际上中序遍历,只需要改变赋值即可
    //中序遍历
    public void inSelect() {

        int[] in = new int[max]; //定义存放的值
        int i = 0;
        //当成栈来使用
        TreeNode[] a = new TreeNode[max]; //设置max是防止数组越界
        int aa = 0; //定义边界下标

        TreeNode treeNode = root;
        //由于次数不固定,所以需要无限循环,因为我们可以随时的添加,但实际上他也可以操作固定规律的或者某些条件的,只要你计算得到一方的执行次数即可
        //这里我就要提一下了,在循环中,我们尽量不要操作无限循环,因为如果在项目中出现问题,那么是非常的严重的,最好找到规律执行有限次数或者操作某些条件,如果实在没有规律或者条件,那么操作无限循环的话
        //最好尽量每个判断都有结尾的break
        if (treeNode == null) {
            System.out.println("[]");
            return;
        }


        //这里需要两个循环,外面的循环是保证回退,里面的循环是保证存放左节点
        while (true) {
            //如果他的根节点是null,那么直接的退出,但这里可能也会判断是否有右节点,导致会进行退出,所以这里还需要加上只有栈有值,就不执行
            if (treeNode == null && aa == 0) {
                break;
            }
            while (true) {
                //如果没有左边节点或者右边节点,那么跳出循环
                if (treeNode == null) {
                    break;
                }

                //存放左边节点(包括根节点)
                a[aa] = treeNode;
                aa++;
                //到另外一个左边节点(从左边开始,因为左在右的前面,这是遍历的规则)
                treeNode = treeNode.left;


                //这个循环可以认为是preSelect(treeNode.left);
                //而循环外面的操作就是preSelect(treeNode.right);

            }

            //上面的循环已经得到了当前根树的所有左边节点了,很明显,根据上面的分析,然后要看他是否有右节点
            //拿取外面的左边节点,栈的第一个,也就是最后一个左边节点
            treeNode = a[--aa];

            //保存当前节点的值
            in[i] = treeNode.data; //这个可以认为是放在中间的打印,因为我们只能根据出栈的节点来操作,否则在前面的话,会打印多次相同的
            //因为他这里就是与递归的一样得到当前,因为递归里面需要跳出,而这里的出栈--aa就是跳出
            i++;

            treeNode = a[aa].right;
            a[aa] = null; //既然是出栈,就要为null(即默认值)

        }
        //外面可以发现,他每次都会一路获取过去的,与回退是一样的了


        String b = "";
        System.out.print("[");
        for (int ii = 0; ii < in.length; ii++) {
            if (ii == in.length - 1) {
                b = b + in[ii] + "]";

            } else {
                b = b + in[ii] + ",";
            }
        }
        System.out.println(b);


    }

    //后序遍历
    public void latSelect() {
        int[] in = new int[max]; //定义存放的值
        int i = 0;
        //当成栈来使用
        TreeNode[] a = new TreeNode[max]; //设置max是防止数组越界
        int aa = 0; //定义边界下标

        TreeNode treeNode = root;
        //由于次数不固定,所以需要无限循环,因为我们可以随时的添加,但实际上他也可以操作固定规律的或者某些条件的,只要你计算得到一方的执行次数即可
        //这里我就要提一下了,在循环中,我们尽量不要操作无限循环,因为如果在项目中出现问题,那么是非常的严重的,最好找到规律执行有限次数或者操作某些条件,如果实在没有规律或者条件,那么操作无限循环的话
        //最好尽量每个判断都有结尾的break
        if (treeNode == null) {
            System.out.println("[]");
            return;
        }

        //用来保存第一个右节点,针对树(子树或者整个树)来说的
        TreeNode pre = null;
        int p = 0; //为了不继续操作父的left,需要使得他直接跳出
        //这里需要两个循环,外面的循环是保证回退,里面的循环是保证存放左节点
        while (true) {
            //如果他的根节点是null,那么直接的退出,但这里可能也会判断是否有右节点,导致会进行退出,所以这里还需要加上只有栈有值,就不执行
            if (treeNode == null && aa == 0) {
                break;
            }
            if (aa == 0 && p == 1) {
                break;
            }
            while (true) {
                //如果没有左边节点或者右边节点,那么跳出循环
                if (treeNode == null) {

                    break;
                }
                if (p == 1) {
                    break;
                }

                //存放左边节点(包括根节点)
                a[aa] = treeNode;
                aa++;
                //到另外一个左边节点(从左边开始,因为左在右的前面,这是遍历的规则)
                treeNode = treeNode.left;


                //这个循环可以认为是preSelect(treeNode.left);
                //而循环外面的操作就是preSelect(treeNode.right);

            }

            //上面的循环已经得到了当前根树的所有左边节点了,很明显,根据上面的分析,然后要看他是否有右节点
            //拿取外面的左边节点,栈的第一个,也就是最后一个左边节点
            treeNode = a[aa - 1].right;
            if (treeNode == null || pre == treeNode) { //如果右边是空的,那么就打印(加上数据)他,但是因为我们并没有出栈对应的有右节点的,那么这里必然不会继续下去,所以需要一个来表示他能够继续下去的条件
                //所以我们要保存其操作的第一个右节点
                pre = a[aa - 1];
                p = 1;
                //保存当前节点的值
                in[i] = a[aa - 1].data; //这个可以认为是放在后面的打印,既然是放在后面,那么对应的有右节点的先不操作出栈,以后再操作出栈,而没有的才直接操作
                i++;
                aa--;
                a[aa] = null; //既然是出栈,就要为null(即默认值)
            } else {
                p = 0;
            }


        }
        //外面可以发现,他每次都会一路获取过去的,与回退是一样的了


        String b = "";
        System.out.print("[");
        for (int ii = 0; ii < in.length; ii++) {
            if (ii == in.length - 1) {
                b = b + in[ii] + "]";

            } else {
                b = b + in[ii] + ",";
            }
        }
        System.out.println(b);


    }

    //查找节点
    public TreeNode getNode(int data) {

        TreeNode treeNode = root;
        if (treeNode == null) {
            System.out.println("没有要查找的节点");
            return null;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要查找的节点");
                return null;
            }
            if (treeNode.data > data) {
                treeNode = treeNode.left;
            } else if (treeNode.data < data) {
                treeNode = treeNode.right;
            } else if (treeNode.data == data) {
                return treeNode;
            }
        }
    }

    //删除节点
    public void delete(int data) {
        TreeNode treeNode = root;
        TreeNode temp1 = null;
        if (treeNode == null) {
            System.out.println("没有要删除的节点");
            return;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要删除的节点");
                return;
            } else if (treeNode.data > data) {
                temp1 = treeNode;
                treeNode = treeNode.left;
            } else if (treeNode.data < data) {
                temp1 = treeNode;
                treeNode = treeNode.right;
            } else if (treeNode.data == data) {
                if (temp1.left == treeNode) {
                    temp1.left = null;
                    return;
                }
                if (temp1.right == treeNode) {
                    temp1.right = null;
                    return;
                }
            }
        }
    }


    //更新节点
    public void update(int data, int da) {
        TreeNode treeNode = root;
        TreeNode temp1 = root;
        TreeNode temp2 = root;
        //定义拐点
        TreeNode aa = null;

        if (treeNode == null) {
            System.out.println("没有要修改的节点");
            return;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要修改的节点");
                return;
            } else if (treeNode.data > data) {
                temp2 = temp1;
                temp1 = treeNode;
                treeNode = treeNode.left;
                if (temp2.right == temp1 && temp1.left == treeNode) {
                    aa = temp2;
                }
            } else if (treeNode.data < data) {
                temp2 = temp1;
                temp1 = treeNode;
                treeNode = treeNode.right;
                if (temp2.left == temp1 && temp1.right == treeNode) {
                    aa = temp2;
                }

            } else if (treeNode.data == data) {

                TreeNode temp3 = treeNode.left;
                TreeNode temp4 = treeNode.right;
                int max;
                int min;
                if (temp3 != null) {
                    while (true) {
                        if (temp3.right == null) {
                            min = temp3.data;
                            break;
                        }
                        temp3 = temp3.right;
                    }
                } else {
                    min = treeNode.data;
                }
                if (temp4 != null) {
                    while (true) {
                        if (temp4.left == null) {
                            max = temp4.data;
                            break;
                        }
                        temp4 = temp4.left;
                    }
                } else {

                    max = treeNode.data;
                }
                if (min == treeNode.data && max == treeNode.data) {
                    if (treeNode == root) {
                        treeNode.data = da;

                        return;
                    }
                } else if (min == treeNode.data) {
                    if (da >= max) {


                        System.out.println("修改数值不合理");
                        return;
                    }
                } else if (max == treeNode.data) {
                    if (da <= min) {


                        System.out.println("修改数值不合理");
                        return;
                    }
                }


                if (temp1.left == treeNode) {
                    if (aa == null) {
                        if (da >= temp1.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    } else {
                        if (da >= temp1.data || da <= aa.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    }
                    return;
                }
                if (temp1.right == treeNode) {
                    if (aa == null) {
                        if (da <= temp1.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    } else {
                        if (da <= temp1.data || da >= aa.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    }
                    return;
                }


            }
        }
    }


    //如果有什么问题,你可以试着修改
    public static void main(String[] args) {
        Tree1 tree = new Tree1();
        tree.insert(10);
        tree.insert(8);
        tree.insert(11);
        tree.insert(7);
        tree.insert(9);
        tree.insert(12);
        /*
                    10
                8       11
              7    9        12
              前序遍历是:10,8,7,9,11,12
              中序遍历是:7,8,9,10,11,12
              后序遍历是:7,9,8,12,11,10
         */
        System.out.println("=============前序遍历是:=============");
        tree.preSelect();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect();
        System.out.println("总个数是:" + tree.max);

        //查找数值为7的节点
        TreeNode node = tree.getNode(7); //若返回null,代表没有该节点
        if (node != null) {
            System.out.println("数值为:" + node.data);
        } else {
            System.out.println("没有节点,也就没有数值");
        }
        //删除数值为7的节点
        tree.delete(7);
        System.out.println("=============前序遍历是:=============");
        tree.preSelect();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect();

        //修改数值为7的节点
        tree.update(8, 9);
        System.out.println("=============前序遍历是:=============");
        tree.preSelect();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect();
    }
}

//至此,我们通过具体思路来使得完成了,在操作算法中,最好先完成具体构思,就如想数学题一样的去想
//到这里,我们已经学习了具体的数学知识(算法),比如跟踪算法,递归,二分法等等这些,通过这些数学知识,我们可以来完成大多数的需求,但程序也是存在数据结构的,所以就是通过这些算法来操作的数据结构,来完成大多数的需求
//所以这里的学习,就相当于学习数学一样,慢慢体会吧,以后出现了问题,看你呢能不能想出来了,实在不行看答案(百度),反正我们是要学习的,总不能无师自通吧(因为无师自通这太难了哦,一般极少数人可以)
但是我们可以发现,使用自己定义的栈来说,实在是过于繁琐,那么我们可以进行修改,操作如下:
package f;

import com.Tree;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 *
 */
public class Tree2 {
    //定义头部节点
    TreeNode root;
    //总共有多少个节点
    int max;

    //定义节点信息,我们使用链表法来操作,因为链表整体上比数组好
    public class TreeNode {

        //定义本身的值
        int data;
        //定义左节点
        TreeNode left;
        TreeNode right;

        public TreeNode(int data) {
            this.data = data;
        }
    }

    //操作插入节点,指定插入的数据,因为是循环,所以我们直接的操作root变量
    public void insert(int data) {
        //如果没有数据,那么直接创建
        if (root == null) {
            root = new TreeNode(data);
            max++;
            return;
        }
        TreeNode temp1 = root;
        while (true) {
            //插入左边
            if (data < temp1.data) {
                if (temp1.left == null) {
                    temp1.left = new TreeNode(data);
                    max++;
                    return;
                } else {
                    temp1 = temp1.left;
                }

            }
            //插入右边
            if (data > temp1.data) {
                if (temp1.right == null) {
                    temp1.right = new TreeNode(data);
                    max++;
                    return;
                } else {
                    temp1 = temp1.right;
                }

            }
        }
    }

    //前序遍历
    public void preSelect() {

        int[] result = new int[max];
        int a = 0;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        if (cur == null) {
            System.out.println("[]");
            return;
        }
        //很明显,这里不操作无限循环了,而是使用自己的判断
        while (null != cur || !stack.isEmpty()) {
            while (null != cur) {
                result[a] = cur.data;
                a++;
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.pop().right;
        }
        String b = "";
        System.out.print("[");
        for (int ii = 0; ii < result.length; ii++) {
            if (ii == result.length - 1) {
                b = b + result[ii] + "]";

            } else {
                b = b + result[ii] + ",";
            }
        }
        System.out.println(b);


    }

    //中序遍历
    public void inSelect() {
        int[] result = new int[max];
        int a = 0;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        if (cur == null) {
            System.out.println("[]");
            return;
        }
        while (null != cur || !stack.isEmpty()) {
            while (null != cur) {
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.pop();
            result[a] = cur.data;
            a++;
            cur = cur.right;
        }

        String b = "";
        System.out.print("[");
        for (int ii = 0; ii < result.length; ii++) {
            if (ii == result.length - 1) {
                b = b + result[ii] + "]";

            } else {
                b = b + result[ii] + ",";
            }
        }
        System.out.println(b);
    }

    //后序遍历
    public void latSelect() {

        int[] result = new int[max];
        int a = 0;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root, pre = null;
        if (cur == null) {
            System.out.println("[]");
            return;
        }

        //这里我们操作具体条件了,而不是将条件放在循环里面
        while (null != cur || !stack.isEmpty()) {
            while (null != cur) {
                stack.push(cur);
                cur = cur.left;
            }

            cur = stack.peek();
            if (null == cur.right || pre == cur.right) {
                result[a] = cur.data;
                a++;
                pre = cur;
                cur = null; //因为栈的存在,所以可以不用操作变量(所以相关代码判断可以删除),在之前的循环中,你可以也这样操作(即可以不用设置变量来操作),因为反正只要执行上面的得到的代码,即之前的代码中的treeNode = a[aa - 1].right;或者这里的cur = stack.peek();(得到栈顶)
                stack.pop();
            } else
                cur = cur.right;
        }
        String b = "";
        System.out.print("[");
        for (int ii = 0; ii < result.length; ii++) {
            if (ii == result.length - 1) {
                b = b + result[ii] + "]";

            } else {
                b = b + result[ii] + ",";
            }
        }
        System.out.println(b);
    }

    //查找节点
    public TreeNode getNode(int data) {

        TreeNode treeNode = root;
        if (treeNode == null) {
            System.out.println("没有要查找的节点");
            return null;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要查找的节点");
                return null;
            }
            if (treeNode.data > data) {
                treeNode = treeNode.left;
            } else if (treeNode.data < data) {
                treeNode = treeNode.right;
            } else if (treeNode.data == data) {
                return treeNode;
            }
        }
    }

    //删除节点
    public void delete(int data) {
        TreeNode treeNode = root;
        TreeNode temp1 = null;
        if (treeNode == null) {
            System.out.println("没有要删除的节点");
            return;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要删除的节点");
                return;
            } else if (treeNode.data > data) {
                temp1 = treeNode;
                treeNode = treeNode.left;
            } else if (treeNode.data < data) {
                temp1 = treeNode;
                treeNode = treeNode.right;
            } else if (treeNode.data == data) {
                if (temp1.left == treeNode) {
                    temp1.left = null;
                    return;
                }
                if (temp1.right == treeNode) {
                    temp1.right = null;
                    return;
                }
            }
        }
    }


    //更新节点
    public void update(int data, int da) {
        TreeNode treeNode = root;
        TreeNode temp1 = root;
        TreeNode temp2 = root;
        //定义拐点
        TreeNode aa = null;

        if (treeNode == null) {
            System.out.println("没有要修改的节点");
            return;
        }
        while (true) {
            if (treeNode == null) {
                System.out.println("没有要修改的节点");
                return;
            } else if (treeNode.data > data) {
                temp2 = temp1;
                temp1 = treeNode;
                treeNode = treeNode.left;
                if (temp2.right == temp1 && temp1.left == treeNode) {
                    aa = temp2;
                }
            } else if (treeNode.data < data) {
                temp2 = temp1;
                temp1 = treeNode;
                treeNode = treeNode.right;
                if (temp2.left == temp1 && temp1.right == treeNode) {
                    aa = temp2;
                }

            } else if (treeNode.data == data) {

                TreeNode temp3 = treeNode.left;
                TreeNode temp4 = treeNode.right;
                int max;
                int min;
                if (temp3 != null) {
                    while (true) {
                        if (temp3.right == null) {
                            min = temp3.data;
                            break;
                        }
                        temp3 = temp3.right;
                    }
                } else {
                    min = treeNode.data;
                }
                if (temp4 != null) {
                    while (true) {
                        if (temp4.left == null) {
                            max = temp4.data;
                            break;
                        }
                        temp4 = temp4.left;
                    }
                } else {

                    max = treeNode.data;
                }
                if (min == treeNode.data && max == treeNode.data) {
                    if (treeNode == root) {
                        treeNode.data = da;

                        return;
                    }
                } else if (min == treeNode.data) {
                    if (da >= max) {


                        System.out.println("修改数值不合理");
                        return;
                    }
                } else if (max == treeNode.data) {
                    if (da <= min) {


                        System.out.println("修改数值不合理");
                        return;
                    }
                }


                if (temp1.left == treeNode) {
                    if (aa == null) {
                        if (da >= temp1.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    } else {
                        if (da >= temp1.data || da <= aa.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    }
                    return;
                }
                if (temp1.right == treeNode) {
                    if (aa == null) {
                        if (da <= temp1.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    } else {
                        if (da <= temp1.data || da >= aa.data) {
                            System.out.println("修改数值不合理");
                        } else {
                            treeNode.data = da;

                        }
                    }
                    return;
                }


            }
        }
    }


    public static void main(String[] args) {
        Tree2 tree = new Tree2();
        tree.insert(10);
        tree.insert(8);
        tree.insert(11);
        tree.insert(7);
        tree.insert(9);
        tree.insert(12);
        /*
                    10
                8       11
              7    9        12
              前序遍历是:10,8,7,9,11,12
              中序遍历是:7,8,9,10,11,12
              后序遍历是:7,9,8,12,11,10
         */
        System.out.println("=============前序遍历是:=============");
        tree.preSelect();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect();
        System.out.println("总个数是:" + tree.max);

        //查找数值为7的节点
        TreeNode node = tree.getNode(7); //若返回null,代表没有该节点
        if (node != null) {
            System.out.println("数值为:" + node.data);
        } else {
            System.out.println("没有节点,也就没有数值");
        }
        //删除数值为7的节点
        tree.delete(7);
        System.out.println("=============前序遍历是:=============");
        tree.preSelect();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect();

        //修改数值为7的节点
        tree.update(8, 9);
        System.out.println("=============前序遍历是:=============");
        tree.preSelect();
        System.out.println("=============中序遍历是:=============");
        tree.inSelect();
        System.out.println("=============后序遍历是:=============");
        tree.latSelect();
    }


}

//这里对前面的循环进行了大致的简化,这样看起来就舒服多了

//那么使用递归好还是使用循环好呢,都基本一样,但是我们只会操作自己认为方便的算法,就如数学题一样,认为方便的解法
//这里我们最好操作递归,因为代码量非常少(因为回退,即使用了方法本身的作用,导致不用栈也行),所以从这里开始,以后也只使用自己方便的操作了

//他们都是不同的算法,只是实现不同而已,即过程不同,这里的实际上具体思路差不多(当然,你也可以有其他思路,只要能实现就行)
这样,我们就使用自带的栈了,大大的减少对应的代码,且不操作无限循环,那么就不用在里面进行判断了
我们通过循环和递归可以发现,的确,某些情况下,循环要复杂一点,因为他没有递归的回退
总体来说,循环的时间复杂度(这里以方法为主),在遍历上,是大致是O(n)(实际上大致是2n,只是对于时间复杂度来说是省略系数的,默认n为无限大来表示时间复杂度,以后都是这样的默认),所以实际上还要多的,因为一个节点有两个下一个,即左边和右边,但添加上是O(logn),因为有排除,n代表节点
删除,查找,都是O(logn)(实际上是xn+y,进行省略x和y了,因为节点之间并不是完全平分,以后会说明的,这个x和y可以是任何系数或者符号),修改也是,但是如果要考虑规则,那么修改可能会变成O(n),因为他里面有个循环需要遍历自身节点
这里要注意:虽然是两个循环,但是我们要考虑变化的,而不是一个循环就一定是n
而递归的时间复杂度(这里以方法为主),我们观察次数,可以发现,他的插入(也就是添加),也是O(logn),即查找,删除都也是O(logn),修改与循环一样的(也是xn+y省略x和y了,因为节点之间并不是完全平分,以后会说明的)
但是遍历,我们可以发现,一个是递归,一个是循环,由于循环是O(n),那么递归呢,我们可以观察发现,也基本是O(n)(两个调用方法),因为思想上基本差不多,所以他们两个基本一样,但是递归代码少,所以我们一般使用递归
所以插入和查找这两个主要的功能就是O(logn)了(虽然删除也是),而不是链表的O(n)了,所以有时候,我们会将链表变成二叉树(如jdk8及其以后版本的hashmap就会变成二叉树,即他是变成红黑树的,后面会说明的),但是二叉树在一定情况下,也会变成链表,比如,都在左边,但我们是整体考虑的,所以一般不会认为是O(n),但是他也会出现这样的情况,因为可以人为,虽然链表在尾部插入也是如此,但链表不用考虑,因为查询仍然是O(n),但是二叉树如果变成这样,对查询来说,就变成了O(n)了,即减低了时间复杂度,所以我们需要解决这样的问题,一般我们会使用平衡二叉树来解决,后面会说明的
当然,如果你有能力,可以为了更加的知道具体打印的形象,编写一个打印方法,而不是像我一样使用数组形式来表示
到这里,我们可以发现,有些数据结构在某些方面比单纯的数组或者链表更加的好(有失必有得,所以有些方面可能不好),但是他们基本都是以数组和链表的方式来完成的
所以我们也称数组和链表是物理结构,而他们是逻辑结构
那么二叉树必然都比链表好吗,答:不一定,对于遍历来说,二叉树是O(n),但实际上确是2n,只是省略了而已,所以在真实比对中,我们不能进行省略,因为对时间复杂度来说是都省略的,可是我们要对比了,那么必然看具体的值,即不能省略,除非不是同级别的,比如O(1),和O(n),即不相同级别对比不省略(因为大的包含小的,必然是差的,所以不用对比就能知道的),而由于链表是O(n),那么由于相同的级别在对比中不能省略,那么就看具体值了,很明显链表要好,因为他只需要判断节点的一个下一个节点,而不是两个,实际上二叉树就是为了保证查询,和插入的效率,才有两个的,但是在某些方面(如遍历)就要牺牲一些时间复杂度了(即提高,而不是减少)
因为时间复杂度越低(少)越好,就比如O(n)比O(n^2)好,即有失必有得,因为你就是使用我来完成的,不交点东西不好吧
有失必有得,是建立在一般情况下的,某些情况下,可能都是得(使用数学,代码又少,效率高,都得了),或者都是失(不使用数学,代码多,效率低,都失了)
最后提一下:如果二叉查找树不平均,那么他的插入,删除,查找会是O(logn)吗,答不会(对比不省略来说),后面会说明的
2:广度优先遍历:
前面我们说明了深度优先遍历的三种(“一头扎到底”),接下来我们来说明广度优先遍历
广度优先遍历也叫层序遍历,顾名思义,就是二叉树按照从根节点到叶子节点的层次关系,一层一层横向遍历各 个节点

在这里插入图片描述

二叉树同一层次的节点之间是没有直接关联的,利用队列可以实现
根节点1进入队列:

在这里插入图片描述

在这里插入图片描述

节点1出队,输出节点1(默认为数值,以后就不做说明),并得到节点1的左孩子节点2、右孩子节点3,让节点2和节点3入队

在这里插入图片描述

在这里插入图片描述

节点2出队,输出节点2,并得到节点2的左孩子节点4、右孩子节点5,让节点4和节点5入队

在这里插入图片描述

节点3出队,输出节点3,并得到节点3的右孩子节点6,让节点6入队

在这里插入图片描述

节点4出队,输出节点4,由于节点4没有孩子节点,所以没有新节点入队

在这里插入图片描述

节点5出队,输出节点5,由于节点5同样没有孩子节点,所以没有新节点入队

在这里插入图片描述

节点6出队,输出节点6,节点6没有孩子节点,没有新节点入队

在这里插入图片描述

我们可以发现,他就是慢慢的输出,也就是从上到下,从左到右的输出,而不是像深度优先遍历一样,按照要求输出(虽然该要求在一定程度上代码非常少,比如递归)
具体实现代码如下:
package com;

import javax.management.Query;
import java.util.LinkedList;
import java.util.Queue;

/**
 *
 */
public class Scope {
    TreeNode root;
    int max;

    public class TreeNode {

        int data;
        TreeNode left;
        TreeNode right;

        public TreeNode(int data) {
            this.data = data;
        }
    }

    //操作插入节点,指定根节点和插入的数据
    public TreeNode insert(TreeNode treeNode, int data) {
        if (treeNode == null) {
            treeNode = new TreeNode(data);
            max++;
            return treeNode;
        }
        if (data < treeNode.data) {

            treeNode.left = insert(treeNode.left, data);
        } else if (data > treeNode.data) {

            treeNode.right = insert(treeNode.right, data);
        } else {
            treeNode.data = data;
        }

        return treeNode;

    }

    //操作广度优先遍历
    public void Scope() {
        Queue<TreeNode> queue = new LinkedList();
        //先将根节点加入队尾
        queue.offer(root); //使用add方法也行,看源码就知道了,他这个offer实际上就是操作add的,只是有些jdk的版本中,offer方法可能会判断是否队列满而不操作添加,而add方法则可能会出现异常,所以一般我们都会使用offer方法
        //如果不等于空,就操作循环
        while (!queue.isEmpty()) {
            //返回队首元素并删除,这里是主要的实现代码,使得必须先操作完上一行的数据节点才可操作下一行(出队造成的),并且会使得上一行在队尾加上其自身的对应的两个节子点,并等待操作
            TreeNode poll = queue.poll();
            System.out.println(poll.data);
            if (poll.left != null) {
                queue.offer(poll.left);
            }
            if (poll.right != null) {
                queue.offer(poll.right);
            }
        }
    }

    public static void main(String[] args) {
        Scope tree = new Scope();
        tree.root = tree.insert(tree.root, 10);
        tree.root = tree.insert(tree.root, 8);
        tree.root = tree.insert(tree.root, 11);
        tree.root = tree.insert(tree.root, 7);
        tree.root = tree.insert(tree.root, 9);
        tree.root = tree.insert(tree.root, 12);
        /*
                    10
                8       11
              7    9        12

         */
        tree.Scope();
        System.out.println("总共多少节点:" + tree.max);

        //这里我就不操作其他的要求了,比如删除,查找,更新等等,你直接复制即可,基本是一样的时间复杂度
        //这里没有操作打印了,你也可以参照之前的打印代码,也复制即可

        //这里很明显,我们是使用队列来保存节点,使得可以操作不相关的节点,就如我们之前操作栈一样,来使得可以操作上一个节点一样,到这里,我们应该知道,为什么学习队列和栈了,因为有些情况就是需要操作先进后出(栈)的思想,前面的深度优先遍历,有些情况需要先进先出的思想,如这里的广度优先遍历
        //之所以是思想,因为在前面我手动用数组来完成了一个,他就是利用栈的思想
        //当你掌握了这些操作后,就相当于你掌握的数学知识,能够在大多数的题目上使用到
    }

}


//所以广度是以中间开始往外扩散,所以是广度,而深度就是一路找过去,所以是深度
我们可以发现,他就是使用循环来操作的,就如我们之前操作栈或者队列(虽然这里队列实现)类似,他的遍历是O(n),n代表节点数
时间复杂度:
以方法为主,插入也是O(logn),注意,他与二分法(二分查找)一样是去除2的这个底数的,因为对时间复杂度来说,默认n是无限大的
这里要提一下(接着前面的疑问),一般情况下,二叉树达到O(logn)基本是理想的状态的(底数是2,省略的),且大多数左右节点的数量可能不相同,并且,父节点与子节点之间一般也不是分一半,所以可以认为其插入,删除,查找,是在O(n)和o(logn)之间的,包括O(n),包括O(logn)(一般节点还要少,所以最好的情况比logn要好,对比来的,不操作省略)
二叉树的应用:
非线性数据:菜单,组织结构、家谱等等
线性数据:二叉查找树,之前我们说过树是非线性的,那么为什么这里说成线性的呢,主要是针对其中的遍历来说,因为遍历的结果我们称为线性(主要是中序遍历)
所以由于二叉查找树是有序的(从左到右是从小到大的),我们只需要中序遍历(因为是左父右,所以就是从小到大),就可以在 O(n) 的时间复杂度内,输出有序的数据序列,我们将这个也可以称为线性的
二叉查找树的性能非常稳定,扩容很方便(因为链表实现)
红黑树 :
在前面我们说过,一个二叉树是可能会变成链表的,这里就给出解决方案
平衡二叉查找树(平衡二叉树,因为二叉树是统称,从这里开始,为了以后更加的好分辨,我们就不用二叉树来统称了):

在这里插入图片描述

这种二叉查找树就基本退化成了链表(11位置的节点不要,就完全变成链表了,二叉树可以这样),由于树的深度变得多了,查找的效率也会大幅下降
所以需要对这种二叉树进行自平衡,红黑树就是一种自平衡的二叉查找树(也可以叫做二叉树,因为是统称)
红黑树(Red Black Tree):
除了二叉查找树(BST)的特征外,还有以下特征(规则,以后会多次说明,特别是规则5):
1:每个节点要么是黑色,要么是红色
2:根节点是黑色
3:每个叶子节点都是黑色的空结点(NIL结点,一般为null),为了简单描述,一般会省略该节点)
4:如果一个节点是红色的,则它的子节点必须是黑色的(一般父也是黑,即父子不能同为红) ,因为总不能也是红的,那么就可以随便添加了,所以规则4和规则5是平衡的主要规则,虽然以规则5为主(即规则5是关键)
注意:这里没有说明一个节点是黑色的规则,即黑色后面可以是黑色,也可以是红色,这里格外注意,主要存在于根节点位置,因为根节点必须为黑,或者结尾位置,因为结尾子节点必然是黑,少部分存在中间
5(这里在后面会多次说明):从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点(平衡的关键) ,正是因为这样,所以可以使得之前说的二叉查找树的其插入,删除,查找,是在O(n)和o(logn)之间的,包括O(n)和O(logn)的O(n)被移除,即不包括O(n)了
并且,偏向于O(logn),而不是原来的都平均,如果说O(n)是10,O(logn)是1,那么可以说原来的是[10~0.99],现在是[5 ~ 0.99],之所以是5(大致的认为的),是因为8这个数,或者这个位置的节点的数值,并不会一定是13这个数的一半(中间),或者这个节点数值的一半(实际上红黑树的自平衡会导致认为是一半的,针对已有节点来说,所以这个5后面会换种解释,即为1.01,这里给出例子:比如1,6,90,对于6来说就是一半,实际上是中间的意思,而不是(1+90)/2的一半,在前面可能使用二分法操作过,实际上也是中间的意思,只是对应的下标刚好是排序的而已,所以一半这个词,无论对应于这里还是之前的二分法,都是中间的意思,所以这个5对于红黑树来说实际上要认为是1.01,之所以是1.01,是因为有x的存在,后面会说明,当然1.01和0.99只是用来证明还是有差一丢丢和好一丢丢的情况的),但我们也可以发现,他偏向于O(logn)了,即减低了时间复杂度,即操作了平衡,实际上这个5(或者1.01)可以计算出来,因为他是平衡的,所以总节点如果是7,那么就是查询三次(很明显的确是1.01,即针对已有节点来的),我们将查询的总节点个数记作n(时间复杂度基本是将总数或者总节点或者总次数作为n的,比如前面的2^x = n的n,即这里我们将总节点数或者总数作为n,比如这里的7),那么查询次数就是logn+1+x(可以多出几个节点,所以有x),这个1在log里面,即这个5就是logn+1+x次,即范围就是[logn+1+x ~ 0.99],但我们可以发现,他可以多出几个节点(比如下面的6位置的节点),那么一般能加上几次,所以是logn+1+x,但整体而言,是logn+1(一般底数也是2,省略了,所以我们默认log在时间复杂度的底数是2,以后默认省略,无论n是否无限大都省略)
他们两个对比,是同级别的,不能省略系数,1和x也不能
而时间复杂度的n默认是无限大的,所以他还是O(logn),只是在对比中,不省略而言(对比没有平衡的)
这里要注意:因为n是7,少于8,所以最好的情况下,比logn要好(对比不省略),即0.99
还要注意:红黑树,一般会使得整体到中间(所以会导致1.01),而不是你的根节点固定或者子树固定,在后面会说明(即左右旋转)
6:新插入节点默认为红色,插入后需要校验红黑树是否符合规则(前面五个特征,或者说规则),不符合则需要进行平衡(或者说使得平衡,相当于做错题,要改正)
一颗典型的红黑树:
很明显,从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点,即3个(包括最后一个null)

在这里插入图片描述

在对红黑树进行添加或者删除操作时可能会破坏这些特点(因为默认插入红色,那么由于红色下面只能是黑色,所以破坏了特点)
那么为什么不默认插入黑色呢,实际上也是一样的,但由于末尾是默认黑色,所以插入红色,当然,他们是相对的,如果上面的特点是以红色为主,那么自然黑色就与这里的红色一样,所以是相对的)
所以红黑树采取了很多方式来维护这些特点,从而维持平衡,主要包括:左旋转、右旋转和颜色反转
左旋(RotateLeft):
逆时针旋转红黑树的两个结点(虽然是旋转,但实际上只是名称而已,具体看他的作用,你甚至可以称为左平衡),使得父结点被自己的右孩子取代,而自己成为自己的左孩子

在这里插入图片描述

上图所示过程如下:
1:以X为基点逆时针旋转
2:X的父节点被x原来的右孩子Y取代
3:c保持不变
4:Y节点原来的左孩子c变成X的右孩子
右旋(RotateRight):
顺时针旋转红黑树的两个结点,使得父结点被自己的左孩子取代,而自己成为自己的右孩子

在这里插入图片描述

上图所示过程如下:
1:以X为基点顺时针旋转
2:X的父节点被x原来的左孩子Y取代
3:b保持不变
4:Y节点原来的右孩子c变成X的左孩子
我们可以发现,他们旋转后,具体的位置大小是不变的,这里需要格外注意:很明显,他这样改变位置,也不会使得大小对比发生变化,相当于x往下移动,只是分左和右而已,且位置改变能够解决一些问题,特别的是x和y改变了位置,在后面会具体说明
颜色反转(满足规则4,规则4是前提,然后满足规则5,主要是规则5,因为最终都要满足,而规则4是统一认为的,所以以后以规则5为主):
就是当前节点与父节点、叔叔节点同为红色,这种情况违反了红黑树的规则,需要将红色向祖辈上传(总不能不会操作吧,那么就变成了可以随便添加了)
即父节点和叔叔节点红色变为黑色,爷爷节点从黑色变为红色(爷爷节点之前必为黑色,因为此前是符合红黑树规则的)
这样每条叶子结点到根节点的黑色节点数量并未发生变化,因此都其他树结构不产生影响,注意了,这里并没有说其祖父之类的节点,所以他只是操作他们子,父,爷这三个
而如果当前节点是红,父节点是黑,那么一般不用变,或者只要刚好满足了规则5,那么就不用变了,后面的构建过程中会提到的
为什么要使用颜色反转呢,可以不使用吗,答:基本不可,否则就不是红黑树的(虽然是命名的,如果节点是蓝和绿,你也可以叫做蓝绿树,只是因为创始人喜欢红和黑,所以是红黑树)
那么使用又有什么好处呢:
他可以用来满足规则5,因为规则5的存在,使得查询(主要是查询,当然其他操作也行的,比如删除)变得平均,之所以变得平均,是因为线路相差不大,那么为什么不使用三个颜色呢,我们知道,因为规则5的原因,所以在一个线上可以比其他线路要多,比如前面的典型的红黑树中,13-8-1-6就是多的一个线,如果操作三个及其以上的颜色,必然,会导致其中短的路径,到长的路径相差很多(如果是黑为主,那么可以加上第三种颜色使得变长),使得总体来说,不够平均,即不稳定,前面也说过,不稳定的最好不要,因为是看运气的,即类似于成功并快速得到数据的概率要小,前面也说明过这样
那么可以只使用一个吗,这里我们假设可以,那么颜色反转就没有必要的,那么当我们添加一个节点,必然会导致规则5不满足,因为你添加的这个线,是比其他线要长的,又因为只有一种颜色,所以不满足规则5,而如果忽略规则5,那么自然就能随便添加了,那么就没有平均可言,所以总体来说,即一个颜色当然不行,因为这样节点基本不能添加了,即不能存放了,所以一般我们只会使用两个颜色,这样使得满足规则5的前提下,最短路径和最长路径是相差最少的,且节点也是最少的
所以颜色反转也是为了使得使用两种颜色,并且使得查询(主要是查询)变平均的操作,因为他是主要操作添加的,简单来说,颜色反转使得操作添加节点时(也可以是其他操作,必然左右旋转),也让颜色分布正确,使得树的节点不会变得很多

在这里插入图片描述

红黑树插入有五种情况,每种情况对应着不同的调整方法:
要注意:不同情况的图片是不相关的,不要以为有联系
1:新结点(A)位于树根,没有父结点
直接让新结点变色为黑色,规则2得到满足
同时,黑色的根结点使得每条路径上的黑色结点数目都增加了1,所以并没有打破规则5

在这里插入图片描述

上面的1,2代表空节点,注意类似的即可
2:新结点(B)的父结点是黑色
新插入的红色结点B并没有打破红黑树的规则,所以不需要做任何调整

在这里插入图片描述

3:新结点(D)的父结点和叔叔结点都是红色
两个红色结点B和D连续,违反了规则4,因此我们先让结点B变为黑色

在这里插入图片描述

这样一来,结点B所在路径凭空多了一个黑色结点,不满足规则5,因此我们让结点A变为红色

在这里插入图片描述

结点A和C又成为了连续的红色结点,我们再让结点C变为黑色

在这里插入图片描述

然后由于根节点必须为黑,那么A变成黑,这样满足规则5了,实际上A可以直接的跳过变成红,而直接让C变成黑来满足规则5(仅限于根节点),一般情况下,必须要触发C变成黑的状态,总不能A是黑,你就变成黑吧,所以他们都是根据规则状态一路变成过去的,所以规则尤为重要,因为没有你是黑,我要变成黑的规则,所以特别的,如果A不是根节点,那么这样的跳过是不可取的
就算是根节点也不可取,只是,我们可以通过程序来跳过而已,但是这是没有必要的,因为规则会自动的操作完成(已有程序实现的规则)
经过上面的调整,这又重新符合了红黑树的规则
4:新结点(D)的父结点是红色,叔叔结点是黑色(虽然这里没有显示,但具体可以看这个博客:https://www.cnblogs.com/Anker/archive/2013/01/30/2882773.html他这里就有例子,后面的构建过程也有例子)或者没有叔叔(这里认为没有叔叔,即没有C,所以下面的C你要省略,因为加上了是不符合规则的,这里要注意,这是图片的问题),且新结点是父结点的右孩子,父结 点(B)是祖父结点的左孩子
我们以结点B为轴,做一次左旋转,使得新结点D成为父结点,原来的父结点B成为D的左孩子

在这里插入图片描述

这样进入了情况5,这里是新的情况,要注意:不同情况的图片是不相关的
5:新结点(D)的父结点是红色,叔叔结点是黑色或者没有叔叔(这里认为没有叔叔,那么c要省略,我们是以c省略来操作的),且新结点是父结点的左孩子,父结 点(B)是祖父结点的左孩子
我们以结点A为轴,做一次右旋转,使得结点B成为祖父结点,结点A成为结点B的右孩子

在这里插入图片描述

接下来,我们让结点B变为黑色,结点A变为红色,也可以认为他们是互相换位置,但是颜色不换,但实际上是为了满足规则5

在这里插入图片描述

经过上面的调整,这一局部重新符合了红黑树的规则,很明显,这个左旋转和右旋转,导致根节点发生改变,因为我们也知道,B是中间的,所以需要变成中间的位置,即前面说明的:
“还要注意:红黑树,一般会使得整体到中间,而不是你的根节点固定或者子树固定,在后面会说明(即左右旋转)”
很明显,左右旋转因为使得节点发生位置改变,所以可以使得根节点发生改变,实际上之所以这样做,是为了使得数据是中间的数据,这样才不会因为一个节点固定,导致出现链表的原因,所以左右旋转是解决固定,而颜色反转是为了操作平均(规则5)
他们一起,使得整个树变的平衡,分开来说,就是左右旋转是使得树结构平衡的具体操作,而颜色反转是使得结构的节点不变多的前提(即添加也会使得颜色满足规则5),其中颜色反转是任何操作都需要判断的,包括左右旋转(虽然主要是添加),即你旋转后,我也要操作颜色反转,使得满足规则5
而因为左右旋转,所以在红黑树里面,基本是没有具体三个一起的(比如A是右节点或者左节点或者A没有其他左节点或者右节点,并且他的右节点是B或者左节点是B,B的右节点是C或者左节点是C),即他们的组合就是三个一起(也包括特殊的,如他们都有左右节点,那么取中间,具体看情况,使用程序判断即可),那么假设都是右,那么B就是父了,实际上从这里也可以看出,左右旋转也是为了符合颜色反转而操作的,所以有时候,颜色反转也是左右旋转的前提,因为你的节点可以变得很长(后面会具体说明),那么不满足红黑树的平衡,所以单纯的颜色反转没有用了需要左右旋转了,而左右旋转也将中间变成上面,这也是使得颜色反转成功的原因,而上面的组合也是红黑树操作左右旋转的条件
那么什么时候操作左右旋转呢,如果不看前面的介绍,很明显,就是单纯颜色反转不能满足规则5了,即满足上面说明的条件即可(就如左节点小于父节点的条件,而进行添加到左的,所以条件可以自己定义,但是红黑树要操作左右旋转,必然也是需要该条件的,即基本固定的,就如左节点添加一样,当然也包括前面左右旋转的条件说明),然后操作中间的值(体会上面的情况5的B节点即可),那么左旋转自然是将节点的右边变成中间,而右旋转是将节点左边变成中间,只要符合是中间,那么就可以使用,或者必须使用,使得解决固定
所以说,有时候左旋转是和右旋转都使得具体节点往上移动,因为中间就是节点的父节点的
根据上面的情况,可以认为,单纯的颜色反转不满足规则5的情况下,才会操作左右旋转,而左右旋转又以中间来操作,使得最容易满足规则5(中间最容易使得高度变低),所以可以看出,他们的共同操作,才形成了红黑树的结构平衡(左右)和访问平均(主要是查询),即平衡,但颜色反转即是前提,也是后提(旋转后也要操作颜色反转),而规则5是颜色反转的前提(他要满足规则5的),所以也是左右旋转的前提,即需要满足规则5,所以在规则5后面,才会具体说明成"平衡的关键",即从而维持平衡,主要包括:左旋转、右旋转和颜色反转
总结:
颜色反转,使得添加或者其他操作时,让规则5进行满足
左右旋转:单纯的颜色反转没有用了,使得长度变少,从而让单纯的颜色反转来让规则5满足
红黑树构建过程(这里的图片是跟着进行的):
如下图(后面会给出图,然后分析):

在这里插入图片描述

上图所示过程如下:
1:新插入节点默认为红色,5<10,插入到左子节点,插入后左子树深度为2(叶子节点黑色+根节点 黑色),这里的叶子节点代表最后的null,这里只是没有显示而已,这里的深度与普通的二叉树的深度意思是不一样的,这里认为是黑色节点的深度
右子树深度也是2(叶子节点黑色+根节点黑色),满足红黑树规则
2:新插入节点为红色,9<10,需要在左子树进行插入,再和5比较,大于5,放到5的右子树中
此时各个叶子节点到根节点的深度依然是2,但5和9两个节点都是红色,不满足规则第4条
那么需要进行左旋、右旋操作,使其符合规则,可以看出经过操作后,左右子树又维持了平衡
再看图:

在这里插入图片描述

上图所示过程如下:
1:插入节点3后,可以看到又不符合红黑树的规则了,而此时的情况,需要采用颜色反转的操作
就是把5、10两个节点变为黑色,5、10的父节点变为红色,但父节点9是根节点,不能为红色
于是再将9变为黑色,这样整个树的深度其实增加了1层
2:继续插入6节点,对树深度没有影响,那么不会操作左右旋转,因为单纯的颜色反转还是满足规则5的(颜色反转那里,也说明了不用变)
3:插入7节点后,6、7节点都为红节点,不满足规则4,需要进行颜色反转调整
也就是7的父节点和 叔叔节点变为黑色,爷爷节点5变为红色,这里就是一个具体例子(只是在前面的c节点必须不存在,因为需要满足规则5)
刚好满足规则5,那么不用变了,在颜色反转那里有过说明

在这里插入图片描述

上图是变色后的
上图所示过程如下:
1:继续插入节点19,对树深度没有影响,红黑树的规则都满足,无需调整
2:插入节点32后,又出现了不满足规则4(需要颜色反转,这是必然的)和5的情况,此时节点32没有叔叔节点,如果颜色反转的话
左右子树的深度就出现不一致的情况,所以需要对爷爷节点进行左旋操作
为什么呢:为什么不将10变成红呢,而必须要有叔叔节点呢,虽然在颜色反转那里有过说明,但是这里给出原因,实际上颜色反转只操作子,父,爷三个节点(颜色反转那里也说明了),也就是说如果19变成黑,那么10不用变(即不满足规则5了),因为颜色反转明确说明,是红则变黑,而没有说明黑变成红,之所以这样规定,是颜色反转的作用,他这样的规定本来是固定的,因为能够解决规则5的问题,而因为固定,所以这里必须要操作左旋转,也就是单纯的颜色反转不能满足规则5了,总不能临时改变规定吧,那么在程序里,基本是不会出现这种情况的,就算可以,需要判断,并且,如果他不操作左旋转或者右旋转,那么这个线(这个路线),必然是无限延长的,且在以后也需要进行左右旋转,所以总体来说,需要左右旋转,因为单纯的颜色反转不满足规则5了
实际上这种情况下,红黑树一般都会操作(所以他在程序里一般是条件,来使得平衡),所以我们也可以将他认为是左右旋转的一个触发条件,就如前面说的,即:
之前说的没有具体三个一起的,这就是原因(他那里有具体说明为什么是左右旋转的条件,就如左节点添加一样固定,逻辑好的话,一下就知道了),或者说,如果是具体三个,必然操作左右旋转,那么在程序里也是这样,而对于颜色反转来说,就需要叔叔节点了,这样就不会使得是具体三个了,所以需要存在叔叔节点,即实际上对于的说明是没有问题的,只是没有具体解释而已,所以我们也规定,如果存在叔叔,进行颜色反转,不存在,那么这种情况自然需要左右旋转,并且你可以认为10的黑不变,而不满足规则5也行(一般是这样的,这也是为了防止以后再次出现的原因,因为程序是一直操作的,无论是颜色反转,还是左右旋转,都是服务于程序的,所以需要综合考虑),所以通常这样考虑,那么既然说到这里,就说明一下,颜色反转为什么只操作子,父,爷三个节点的
原因如下:
首先,要操作颜色反转,必然是黑,红,红,其中,我们对这三个操作,无论你怎么操作,都只是一个黑,但是如果多出一个,那么可能会变成两个黑了,所以在颜色反转那里也说明了:这样每条叶子结点到根节点的黑色节点数量并未发生变化,因此都其他树结构不产生影响,这就是原因
3:父节点取代爷爷节点的位置,父节点变为黑色,爷爷节点变为父节点的左子树变为红色
再看图:

在这里插入图片描述

上图所示过程如下:
1:插入节点24后,红黑树不满足规则4,需要调整,然后看是否满足规则5(所以也可以说规则4是规则5的前提)
2:此时父节点32和叔叔节点10都为红色,需要进行颜色反转,爷爷节点19变为红色,父节点、叔叔节点变为黑色,颜色反转树的深度不发生变化
再看图:

在这里插入图片描述

上图所示过程如下:
1:插入节点17后,未破坏红黑树规则,不需要调整
到这里,我们可以看到,因为颜色反转,以及左右旋转的操作,使得这个红黑树来满足其规则,当然他们自身的操作也需要一定合理性,比如什么操作会使得哪个节点变成黑或者红等等(比如前面的右旋转,原来的黑变成红,原来的红变成黑),因为既然你制定了规则,那么必然需要操作来满足,就如之前的二叉查找树的规则一样,小的放在左,那么在程序里面也要这样判断,来使得满足规则,所以红黑树也是逻辑结构,而他的规则满足就是需要操作颜色反转和左右旋转,具体原因在前面已经说明了,你也可以认为必须这样
所以说,接下来的代码编写,我们就需要利用颜色反转,左右旋转来满足红黑树了,当出现一些情况时,可以利用程序加几个判断即可,来满足其条件或者规则就行,即随机应变,但颜色反转和左右旋转基本有对应的判断,所以通常不会出现问题,可能没有特别情况,比如都有左右节点,那么加上判断即可,因为操作是来满足规则的,而不是只使用其固定死的其中说明的条件,要随机应变哦
实际上,与其说左右旋转是有多个条件的,还不如说,是因为颜色反转操作不了,才会操作左右旋转的,虽然颜色反转一直判断
但他们都只是满足红黑树的规则而已,是红黑树的规则产生了他们,而不是他们产生了红黑树,就如栈和队列,一样,对应的方法是栈和队列产生的,所以颜色反转,左右旋转也是如此
具体代码编写如下(如果可以的话,最好自己也手写一个,来加深理解,你也自己来手写一个吧):
/**
 *
 */
public class Tree {
    //代表根节点
    Node root;


    //定义节点信息
    public class Node {
        //定义三个节点变量,因为操作红黑树需要子,父,爷三个节点,所以除了自身的左右节点(子节点)外,还需要自己的父子,那么相对于其子来说,就是爷爷
        //所以为了以后不在程序里进行操作临时变量或者编写需要找出他的父节点,那么我们自定义一个父节点信息,用来保存父节点
        Node left;
        Node right;
        Node parent;
        //定义颜色
        boolean color; //默认true为黑色,false为红色
        //定义节点信息
        int key;

        public Node(int key) {
            this.key = key;
            //因为新的节点默认为红色,所以这里进行设置
            this.color = false;
        }

        public String toString() {
            return "Tree{" +
                    "key=" + key +
                    ", color=" + (color == true ? "BLACK" : "RED") +
                    '}';
        }
    }

    //接下来我们来完成红黑树,为了完成红黑树,自然需要满足其规则,我们需要利用颜色反转,左右旋转来进行操作
    //当然,他们是名词,那么在程序里面,就是对应与判断了,所以实际上就是利用其原理,来完成程序的编写判断,当然,如果出现特殊情况,可以自己进行解决

    //首先,我们要进行添加节点
    public void insert(int key) {
        Node temp1 = root;
        if (temp1 == null) {
            root = new Node(key);
            root.color = true; //因为默认是false,所以需要修改
            //他没有父节点,所以是null
            return;
        }
        Node parent = root;

        while (temp1 != null) {
            if (key < temp1.key) {
                parent = temp1;
                temp1 = temp1.left;
            } else if (key > temp1.key) {
                parent = temp1;
                temp1 = temp1.right;
            } else {
                temp1.key = key;
                return; //既然是相同的,那么后续就不用操作了,直接覆盖即可,因为需要满足二叉查找树,所以红黑树也需要这样
            }
            //很明显,如果他们的左节点或者右节点为null,那么就认为其父是最后一个节点了(对于null来说,就是父)
        }
        Node node = new Node(key);
        if (key < parent.key) {
            parent.left = node;
        }
        if (key > parent.key) {
            parent.right = node;
        }
        node.parent = parent;


        //上面我们已经添加完毕,但是我们需要判断是否要颜色反转
        //颜色反转就要看加入的节点的父节点是黑还是红了,当然,为了操作颜色反转,自然最好设置一个方法来操作,因为不只是添加可能会使用,其他操作可能也会使用,当然,左右旋转也是一样的
        banlanceInsert(node);


    }

    //颜色反转
    public void banlanceInsert(Node node) {
        if (node.parent.color == true) {
            return; //是黑的,不用操作
        }
        //是红色的,就需要操作了
        //但是如果他的颜色反转导致了其他节点也会出现颜色为两个红的,那么需要再次的操作,所以需要循环
        //只要对应节点的父节点是黑色的就结束循环,因为不用颜色反转了
        while (node.parent != null && node.parent.color != true) { //之所以加上node.parent!=null,是防止后面的node变成头节点的情况
            //定义爷爷
            Node grandparent = node.parent.parent;
            //我们首先看看我们添加的节点的父节点是其爷爷的左节点还是右节点
            if (grandparent.left == node.parent) {
                //既然是左节点,首先看看有没有叔叔节点

                if (grandparent.right != null && grandparent.right.color == false) {
                    //既然有叔叔节点,直接颜色反转即可,这里要考虑叔叔是否是红还是黑的问题,虽然在对应规则下,添加的地方,叔叔和父基本是相同的红,所以变成黑即可
                    //但可能会出现某些情况,比如你一直往右边添加大的数时,颜色反转会导致出现叔叔节点是黑的情况
                    //如果这时你继续单纯的颜色反转,你会发现,原来参与子,父,爷的三个节点,其中一个参与了他的颜色反转,所以如果单纯的操作自然会导致规则5不满足,所以这里需要进行判断是否为红,即这里也是左右旋转的一个条件,这就是需要随机应变的要求

                    node.parent.color = true;
                    grandparent.right.color = true;
                    grandparent.color = false;
                    //然后将自己的当前节点变量进行赋值,来继续操作循环
                    node = grandparent;
                    continue; //结束循环
                    //到这里可以发现,与其说,左右旋转是有多个条件的,还不如说,因为颜色反转操作不了,才会操作左右旋转的,所以我们可以不只看条件,只要看颜色反转即可
                }
                //没有叔叔节点或者叔叔节点为黑(前面也说明过),那么就是要操作左右旋转,这是左右旋转的条件,在前面已经说明了
                //那么操作哪个旋转呢,或者说,先操作哪个旋转呢,那么需要判断当前节点是父节点的左节点还是右节点了
                //如果是右节点
                if (node == node.parent.right) {
                    //那么操作左旋转,注意由于旋转的条件有多种,需要随机应变,但是,根据上面说的,颜色反转操作不了的,那么基本需要左右旋转,所以这里我们直接旋转即可
                    //当然,如果看到有些问题,随机应变即可
                    Node temp = node.parent;
                    leftRotate(node);
                    //左旋转操作后,很明显,我们需要右旋转
                    rightRotate(temp);


                } else {
                    rightRotate(node);
                }
            } else {
                //与前面基本类似
                if (grandparent.left != null && grandparent.left.color == false) {

                    node.parent.color = true;
                    grandparent.left.color = true;
                    grandparent.color = false;

                    node = grandparent;
                    continue; //结束循环

                }


                if (node == node.parent.right) {

                    leftRotate(node);


                } else {
                    Node temp = node.parent;
                    rightRotate(node);
                    leftRotate(temp);

                }
            }
        }
        root.color = true;
    }

    //左旋转
    public void leftRotate(Node node) {
        //左旋转,无非就是将我们所操作的具体节点进行移动
        //得到父节点
        Node parent = node.parent;
        //得到爷爷节点
        Node grandparent = parent.parent;
        //操作左旋转,自然是操作中间的,那么爷爷节点,需要在父节点的下面,很明显,这里也需要爷爷节点的父子用来表示操作指向
        if (grandparent.parent == null) {
            //如果是null,说明,爷爷节点就是root
            //那么很明显,添加的节点就是一边的第三个
            //那么我们先进行旋转
            //这里还需要进行判断,左旋转的条件
            //如果爷爷节点的左边是父节点,父节点的右边是添加的节点,那么在爷爷的父是null的情况,需要如下操作
            if (node.parent.right == node && grandparent.left == node.parent) {
                Node node1 = node.left; //得到初始节点
                node.left = node.parent; //改变指向
                node.parent.parent = node; //改变头
                node.parent.right = node1; //这里要得到原来节点的左边值,虽然null情况下为null
                if(node1!=null){
                    node1.parent = node.parent; //也改变头
                }
                grandparent.left = node; //改变爷爷指向
                node.parent = grandparent; //改变我的头
                //后续还是需要右转的
                return;
            }
            if (node.parent.right == node && grandparent.right == node.parent) {
                //这样就代表直接的变成root了
                root = node.parent; //定义root
                grandparent.right = node.parent.left; //改变指向
                if( node.parent.left!=null) {
                    node.parent.left.parent = grandparent; //改变指向
                }
                node.parent.parent = null; //改变头
                node.parent.left = grandparent; //改变指向
                grandparent.parent = node.parent; //改变头
                grandparent.color = false; //改变颜色
                root.color = true;
                return;
                //后面的基本类似,就不操作具体注释了
            }
        } else {
            if (node.parent.right == node && grandparent.left == node.parent) {
                Node node1 = node.left;
                node.left = node.parent;
                node.parent.parent = node;
                node.parent.right = node1; //这里要得到原来节点的左边值,虽然null情况下为null
                if(node1!=null){
                    node1.parent = node.parent;  //改变指向
                }
                grandparent.left = node;
                node.parent = grandparent;
                return;
            }
            if (node.parent.right == node && grandparent.right == node.parent) {
                if (grandparent.parent.left == grandparent) {

                    grandparent.parent.left = node.parent;
                    grandparent.right = node.parent.left; //改变指向
                    if(node.parent.left!=null) {
                        node.parent.left.parent = grandparent; //改变头
                    }
                    node.parent.parent = grandparent.parent;
                    node.parent.left = grandparent;
                    grandparent.parent = node.parent;
                    grandparent.color = false; //改变颜色
                    grandparent.parent.color = true;
                    return;
                }
                if (grandparent.parent.right == grandparent) {
                    grandparent.parent.right = node.parent;
                    grandparent.right = node.parent.left; //改变指向
                    if(node.parent.left!=null) {
                        node.parent.left.parent = grandparent; //改变头
                    }
                    node.parent.parent = grandparent.parent;
                    node.parent.left = grandparent;
                    grandparent.parent = node.parent;
                    grandparent.color = false; //改变颜色
                    grandparent.parent.color = true;
                    return;
                }
            }
        }

    }

    //右旋转,与左旋转基本一样,只是条件不同
    public void rightRotate(Node node) {
        //得到父节点
        Node parent = node.parent;
        //得到爷爷节点
        Node grandparent = parent.parent;
        if (grandparent.parent == null) {
            //这里的代码主要是改变位置,以及头部的赋值,你大致看一下吧
            if (node.parent.left == node && grandparent.right == node.parent) {
                Node node1 = node.right; //原来的节点
                node.right = node.parent; //将中间的赋值
                node.parent.parent = node; //改变他的头
                node.parent.left = node1; //赋值原本的,这里可以看之前的左右旋转的图片
                if(node1!=null){ //进行判断,如果是null,那么什么都不用变,否则需要设置头
                    node1.parent = node.parent;
                }
                grandparent.right = node; //爷爷改变指向
                node.parent = grandparent; //对应头变成爷爷
                //这里给出基本的流程,那么后续的代码就不操作具体注释了,前面的左旋转的流程基本也是这样,你自己理解吧


                return;
            }
            if (node.parent.left == node && grandparent.left == node.parent) {
                //这样就代表直接的变成root了
                Node node1 = node.parent.right;
                root = node.parent;
                grandparent.left = node1; //改变指向
                if(node1!=null){
                    node1.parent = grandparent;
                }
                node.parent.parent = null; //改变头
                node.parent.right = grandparent;
                grandparent.parent = node.parent;
                grandparent.color = false; //改变颜色
                root.color = true;
                return;
            }
        } else {
            if (node.parent.left == node && grandparent.right == node.parent) {

                Node node1 = node.right;
                node.right = node.parent;
                node.parent.parent = node; //改变头
                node.parent.left = node1;
                if(node1!=null){
                    node1.parent = node.parent; //改变指向
                }
                grandparent.right = node;
                node.parent = grandparent;

                return;
            }
            if (node.parent.left == node && grandparent.left == node.parent) {
                if (grandparent.parent.left == grandparent) {


                    Node node1 = node.parent.right;
                    grandparent.parent.left = node.parent;
                    node.parent.parent = grandparent.parent;
                    grandparent.left = node1;
                    if(node1!=null){
                        node1.parent = grandparent; //改变指向
                    }
                    grandparent.parent = node.parent;
                    node.parent.right = grandparent;
                    grandparent.color = false; //改变颜色
                    grandparent.parent.color = true;
                    return;
                }
                if (grandparent.parent.right == grandparent) {


                    Node node1 = node.parent.right;
                    grandparent.parent.right = node.parent;
                    node.parent.parent = grandparent.parent;
                    grandparent.left = node1;
                    if(node1!=null){
                        node1.parent = grandparent; //改变指向
                    }
                    grandparent.parent = node.parent;
                    node.parent.right = grandparent;
                    grandparent.color = false; //改变颜色
                    grandparent.parent.color = true;
                    return;
                }
            }
        }
    }

    //上面你应该可以操作提取代码,因为有多个重复的,这里我就不操作了
    //遍历
    public void list(Node node) {
        if (node == null) {
            return;
        }
        if (node.left == null && node.right == null) {
            System.out.println(node);
            return;
        }
        System.out.println(node);
        //递归 左孩子
        list(node.left);
        //递归 右孩子
        list(node.right);

    }

    public static void main(String[] args) {
        Tree rb = new Tree();
        rb.insert(10);//根节点
        rb.insert(5);
        rb.insert(9);
        rb.insert(3);
        rb.insert(6);
        rb.insert(7);
        rb.insert(19);
        rb.insert(32);
        rb.insert(24);
        rb.insert(17);
        rb.list(rb.root);
    }

}


//左右旋转的对应代码,大致可以提取成这样:
   //左旋转
    public void leftRotate(Node node) {
        //左旋转,无非就是将我们所操作的具体节点进行移动
        //得到父节点
        Node parent = node.parent;
        //得到爷爷节点
        Node grandparent = parent.parent;
        if (node.parent.right == node && grandparent.left == node.parent) { //实际上node.parent.right == node可以不用判断,因为要操作左旋转的必然是这种情况,所以这里可以删除,同理右旋转也是一样的
            Node node1 = node.left;
            node.left = node.parent;
            node.parent.parent = node;
            node.parent.right = node1;
            if(node1!=null){
                    node1.parent = node.parent;
            }
            grandparent.left = node;
            node.parent = grandparent;
            return;
        }
        if (node.parent.right == node && grandparent.right == node.parent) {
            if (grandparent.parent == null) {

                //这样就代表直接的变成root了
                root = node.parent;
                node.parent.parent = null;

            } else {
                if (grandparent.parent.left == grandparent) {
                    grandparent.parent.left = node.parent;
                }
                if (grandparent.parent.right == grandparent) {
                    grandparent.parent.right = node.parent;
                }
                node.parent.parent = grandparent.parent;
               
            }
        }
        
        grandparent.right = node.parent.left;
        if( node.parent.left!=null) {
              node.parent.left.parent = grandparent;
        }
        node.parent.left = grandparent;
        grandparent.parent = node.parent;
        grandparent.color = false;
        if (grandparent.parent != null) {
            grandparent.parent.color = true;
        } else {
            root.color = true;
        }

    }

    //右旋转,与左旋转基本一样,只是条件不同
    public void rightRotate(Node node) {
        //得到父节点
        Node parent = node.parent;
        //得到爷爷节点
        Node grandparent = parent.parent;
        if (node.parent.left == node && grandparent.right == node.parent) {
            Node node1 = node.right;
            node.right = node.parent;
            node.parent.parent = node;
            node.parent.left = node1;
            if(node1!=null){
                    node1.parent = node.parent;
                }
            grandparent.right = node;
            node.parent = grandparent;
            return;
        }
        if (node.parent.left == node && grandparent.left == node.parent) {
            Node node1 = node.parent.right;
            if (grandparent.parent == null) {
                root = node.parent;
                node.parent.parent = null;
            } else {

                if (grandparent.parent.left == grandparent) {
                    grandparent.parent.left = node.parent;
                }
                if (grandparent.parent.right == grandparent) {
                    grandparent.parent.right = node.parent;
                }
                node.parent.parent = grandparent.parent;
            }
            grandparent.left = node1;
            if(node1!=null){
                        node1.parent = grandparent;
                    }
            grandparent.parent = node.parent;
            node.parent.right = grandparent;
            grandparent.color = false;
            if (grandparent.parent == null) {
                root.color = true;
            } else {
                grandparent.parent.color = true;
            }
        }
    }

//注意:可能有某些问题,如果你认为有某些逻辑问题可以选择进行修改一下,但大致是没有问题的
为了可以更加直观的知道测试结果,可以到这个网站去测试:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
找到这个:Red-Black Trees,点击后,自行进行添加,与打印结果进行对比,如果对比成功,那么代表正确,上面的代码是我基本按照具体规则编写的,一般情况下,基本是能够完成红黑树的,我测试时,并没有错误,当然,上面的左右旋转,是可以进行提取的,这里就不操作了
至此红黑树操作完成,这里也有个代码可以参考一下,虽然代码不同,但是实现的方式是相同的,但代码简单很多,后面会有注释说明:
package tt;

/**
 *
 */
public class Tree {
    public class RBTreeNode {
        private int key;
        private boolean isBlack;
        private RBTreeNode left;
        private RBTreeNode right;
        private RBTreeNode parent;

        public RBTreeNode(int key) {
            this.key = key;
            this.isBlack = false; //新节点默认是红色

        }

        public int getKey() {
            return key;
        }

        public boolean isBlack() {
            return isBlack;
        }

        public RBTreeNode getLeft() {
            return left;
        }

        public RBTreeNode getRight() {
            return right;
        }

        public RBTreeNode getParent() {
            return parent;
        }

        public void setKey(int key) {
            this.key = key;
        }

        public void setBlack(boolean black) {
            isBlack = black;
        }

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

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

        public void setParent(RBTreeNode parent) {
            this.parent = parent;
        }

        @Override
        public String toString() {
            return "RBTreeNode{" +
                    "key=" + key +
                    ", color=" + (isBlack == true ? "BLACK" : "RED") +
                    '}';
        }
    }

    RBTreeNode root; //根节点

    /**
     * 遍历节点
     */

    public void list(RBTreeNode node) {
        if (node == null) return;
        //叶子

        if (node.getLeft() == null && node.getRight() == null) {
            System.out.println(node);
            return;
        }
        System.out.println(node);
        //递归 左孩子

        list(node.getLeft());
        //递归 右孩子

        list(node.getRight());
    }

    public void insert(int key) {
        RBTreeNode node = new RBTreeNode(key);
        if (root == null) {
            node.setBlack(true);//根是黑色的

            root = node;
            return;
        }
        RBTreeNode parent = root;
        RBTreeNode son = null;
        if (key <= parent.getKey()) {
            son = parent.getLeft();
        } else {
            son = parent.getRight();
        }
        //find the position

        while (son != null) {
            parent = son;
            if (key <= parent.getKey()) {
                son = parent.getLeft();
            } else {
                son = parent.getRight();
            }
        }
        if (key <= parent.getKey()) {
            parent.setLeft(node);
        } else {
            parent.setRight(node);
        }
        node.setParent(parent);
        // 自平衡

        banlanceInsert(node);
    }

    
    //上面的代码与之前的基本类似,主要是后面的颜色反转,左右旋转,即自平衡不同
    /**
     * 自平衡
     *
     * @param node
     */

    private void banlanceInsert(RBTreeNode node) {
        RBTreeNode father, grandFather;
        //只要当前father不是null即可,虽然他可能就是根节点,但我之前的操作,是直接获得爷爷的,而不是这里的中间,即父,所以代码不同
        while ((father = node.getParent()) != null && father.isBlack() == false) {
            grandFather = father.getParent();
            //父为祖左孩子

            if (grandFather.getLeft() == father) {
                RBTreeNode uncle = grandFather.getRight();
                if (uncle != null && uncle.isBlack() == false) {
                    setBlack(father);
                    setBlack(uncle);
                    setRed(grandFather);
                    node = grandFather;
                    continue;
                    //这里的代码差不多
                }
                if (node == father.getRight()) {
                    //左旋

                    leftRotate(father);
                    RBTreeNode tmp = node;
                    node = father;
                    father = tmp;
                     //这里相当于操作右旋的参数,当然,因为右旋可以提取,所以这里进行了提取
                }
                setBlack(father);
                setRed(grandFather);
                //右旋

                rightRotate(grandFather);
            }
            //父为祖右孩子

            else {
                RBTreeNode uncle = grandFather.getLeft();
                if (uncle != null && uncle.isBlack() == false) {
                    setBlack(father);
                    setBlack(uncle);
                    setRed(grandFather);
                    node = grandFather;
                    continue;
                }
                if (node == father.getLeft()) {
                    //右旋

                    rightRotate(father);
                    RBTreeNode tmp = node;
                    node = father;
                    father = tmp;
                }
                setBlack(father);
                setRed(grandFather);
                //左旋

                leftRotate(grandFather);
            }
        }
        setBlack(root);
    }

    /**
     * 左旋
     *
     * @param node
     */

    private void leftRotate(RBTreeNode node) {
        RBTreeNode right = node.getRight();
        RBTreeNode parent = node.getParent();
        //他的定义初始变量与我不同,但是可以发现,后面的操作就是我之前的可以提取后的操作,只是在关键地方进行了判断,你进行调试就知道了,这里就不多说了,之所以这里的代码可以提取这样,是因为他是始终操作中间的节点,而不是最下面的节点,即不用跳节点访问,所以代码少,而我们由于都操作最下面的节点,所以有些地方提取不了,相当于原来的1,2,3,中操作1,2,我们一直取1操作上,而他取2来操作上下,当变成操作2,3时,我们还是取1操作上,他确操作3操作上下,所以他可以完成头和子的完全操作,即节省了很多代码,而不用我们自己操作多个头节点嵌套,他始终都是上和下,所以代码基本一致,而不会导致有两个判断(因为原来只需要操作2头,而后一个确需要3的头了,这是两个不同代码,而使用他这个方式,就同一套代码即可,因为原来是2,变成了3,所以起点变了,而不是之前的起点没有变,所以代码的确省略很多),当然,这里的说明可能对你并没有什么作用,主要看你去调试这里的代码了,看自己的理解了
        //上面的原来是2,然后是3,都是改变原来的最下面节点,然后操作他的父节点,并且会判断是否存在的问题,所以逻辑没有错
        if (parent == null) { //相当于前面的空的判断
            root = right;
            right.setParent(null);
        } else {
            if (parent.getLeft() != null && parent.getLeft() == node) { //相当于判断
                parent.setLeft(right);
            } else {
                parent.setRight(right);
            }
            right.setParent(parent);
        }
        node.setParent(right);
        node.setRight(right.getLeft());
        if (right.getLeft() != null) { //相当于前面的赋值头部判断
            right.getLeft().setParent(node);
        }
        //上面三个判断,对应与我之前写的三个判断,因为我没有操作中间,所以中间的判断,有两个代码(而不是一个代码,只改变部分),即基本分开了(而不是只改变部分小的分开)
        //那里没有操作else(虽然也可以,但我操作了return也是一样的)
        right.setLeft(node);
    }

    /**
     * 右旋
     *
     * @param node
     */

    private void rightRotate(RBTreeNode node) {
        RBTreeNode left = node.getLeft();
        RBTreeNode parent = node.getParent();
        if (parent == null) {
            root = left;
            left.setParent(null);
        } else {
            if (parent.getLeft() != null && parent.getLeft() == node) {
                parent.setLeft(left);
            } else {
                parent.setRight(left);
            }
            left.setParent(parent);
        }
        node.setParent(left);
        node.setLeft(left.getRight());
        if (left.getRight() != null) {
            left.getRight().setParent(node);
        }
        left.setRight(node);
    }

    private void setBlack(RBTreeNode node) {
        node.setBlack(true);
    }

    private void setRed(RBTreeNode node) {
        node.setBlack(false);
    }

    public static void main(String[] args) {
        Tree rb = new Tree();
        rb.insert(10);//根节点
        rb.insert(5);
        rb.insert(9);
        rb.insert(3);
        rb.insert(6);
        rb.insert(7);
        rb.insert(19);
        rb.insert(32);
        rb.insert(24);
        rb.insert(17);
        rb.list(rb.root);
    }
}

//综上所述:为了实现红黑树,的确需要对应的颜色反转和左右旋转,其中具体的判断需要很多细节,比如左右旋转中,叔叔节点是黑或者没有,等等细节,而在前面也大致说明过了,当然,如果代码有误,可以自己调试修改

时间复杂度:O(logn)(针对查询来说),这是不用说的,前面已经说明过了,当然,这里并没有操作查询的操作,因为上面只是创建红黑树,查询的操作就与之前的二叉树的查询是一样的,复制粘贴即可
应用:
在JDK1.8中HashMap使用数组+链表+红黑树的数据结构,内部维护着一个数组table
该数组保存着每 个链表的表头结点或者树的根节点(相当于上面保存根节点的类),HashMap存储数据的数组定义如下,里面存放的是Node实体:
transient Node<K, V>[] table;//序列化时不自动保存,当一个对象被序列化的时候,transient型变量的值不包括在序列化的结果中

/*** 桶的树化阈值:即 链表转成红黑树的阈值, * 在存储数据时,当链表长度 > 该值时(注意是大于),则将链表转换
成红黑树 */ static final int TREEIFY_THRESHOLD = 8;
//我们可以认为他是将值从头到尾的添加到红黑树中,因为红黑树会自动的平衡

//那么到这里,你也应该清楚,红黑树的创始人有多厉害了,因为他们可以创建红黑树的规则使得平衡,并且也创建了颜色反转和左右旋转的操作就能使得平衡,当然,并不是一下子就知道了,也是经过大量的计算,可以认为是思考,就如题目一样,或者试验多种方法或者方式,最终得出红黑树规则,及其满足情况(即左右旋转,颜色反转,他们基本就能满足的)

//那么为什么要变成红黑树,且为什么是大于8(没有等于),会退化成链表吗
//针对这些问题,实际上是空间和时间的概率,我们知道红黑树一个节点,所占用的空间是比一个链表所占用的空间要大的
//一般是占用两倍(有左右节点的情况,以这种情况为主),否则就是多一个变量的空间,但是以节点自身来看,红黑树是一个节点类保存两个节点类的,所以一般我们也认为是一半,但是实际上若是相同的节点数量,所占空间基本相同,只是红黑树需要进行操作才可(这里需要时间)
//即一般情况下,如果以单个节点而已,红黑树的确是链表的两倍,若是总体而言,基本相同
//而至于为什么是8,我们一般的哈希值结果,通常不会集中在一个下标中,且也为了防止自身定义不好的哈希算法而进行操作,所以是8(这是概率问题,即他的概率在自带的哈希值中,比较小,且为了不会变得很多,即操作自定义的哈希算法导致超出,所以取8比较平均,即防止我们自己定义的哈希算法不好,导致集中一起而形成的,使得添加操作不那么困难,因为链表一般需要判断是否相同),并且当红黑树的节点小于或等于 6 个以后,又会恢复为链表形态
//即在8以及8之前使用链表是因为还不需要时间效率,因为红黑树创建需要时间,而之后就需要红黑树了
//而之所以小于6之后恢复链表,是因为时间基本没有差别,但是红黑树需要的判断多(这样的平衡状态),而之所以不到6进行变成红黑树,那么就是上面说的概率问题了

//具体情况:可以百度,有大量类似的文章,你可以选择看这个:http://t.zoukankan.com/liaowenhui-p-15055596.html
至此红黑树大致说明完毕,虽然前面我自己编写了一个代码,但是由于功能实现,那么也行,虽然可能有隐患(测试没有问题,所以实际上可以认为没有隐患),但是自己写的代码有助于理解红黑树的程序操作,你也自己来手写一个吧
多路查找树(也称多路树,注意:他只是在普通的二叉树的另一种而已,而没有其他的限制,比如二叉查找树的限制,即虽然他也有查找两个字,但是多路树和多路查找树是等价的,而不是像二叉树包含二叉查找树一样的包含关系):
多路查找树(muitl-way search tree),其每一个节点的孩子数可以多于两个,且每一个节点处可以存储多个元素,那么就要考虑这些数据的范围了,两个数据自然有三个范围,看后面的B树图就知道了
B树:
B树(BalanceTree)是对二叉查找树的改进,它的设计思想是,将相关数据尽量集中在一起,以便一 次读取多个数据,若数据在磁盘里面,那么可以有效的减少硬盘操作次数
一棵m阶的B 树 (m叉树)的特性如下:
B树中所有节点的孩子节点数中的最大值称为B树的阶,记为M(即最大只能有M个子节点,包括M个,就是下面一条说明)
树中的每个节点至多有M棵子树,即:如果定了M,则这个B树中任何节点的子节点数量都不能超过M
下面两个是规定,保证树的合理性,即数据的最小保存(具体原因可以百度)
若根节点不是终端节点,则至少有两棵子树
除根节点和叶子节点外,所有点(即中间节点)至少有m/2棵子树
所有的叶子结点(结点也称为节点,他们是一样的意思)都位于同一层

在这里插入图片描述

B+树:
B+树是B-树的变体,也是一种多路搜索树,其定义基本与B树相同,它的自身特征是:
非叶子结点的子树指针与关键字个数相同
非叶子结点的子树指针P[i],指向关键字值属于[K[i],K[i+1])的子树
为所有叶子结点增加一个链指针
所有关键字都在叶子结点出现
简单来说,就是在B树上,直接结果放在叶子节点,而前面的指针用来确定位置,并且他的指针节点通常按照左相同放右,或者右相同,放左,或者都进行存放

在这里插入图片描述

数据结构和算法的可视化网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html,这里先放在这里,虽然前面也给出过
最好你自己手动的操作B树和B+树的可视化操作,有利于你理解,一般都是向上移动指针的,然后向下进行分叉
很明显,通过测试,实际上他的代码也不难编写,因为他的这个分叉比红黑树对应的颜色反转,以及左右旋转要容易许多,这里就不给出具体操作了,自己测试吧,因为数据结构有非常多的,所以这里就不给出其中的代码来测试了,因为在前面也说明过:“树的种类非常多,我们会选几个有代表性的详细讲解”
典型应用:
MySQL索引B+Tree
B树是为了磁盘或其它存储设备而设计的一种多叉(下面你会看到,相对于二叉,B树每个内结点有多个 分支,即多叉)平衡查找树,即多叉平衡
B树的高度一般都是在2-4这个高度,树的高度直接影响IO读写的次数
如果是三层树结构,支撑的数据可以达到20G,如果是四层树结构,支撑的数据可以达到几十T
B和B+的区别:
B树和B+树的最大区别在于非叶子节点是否存储数据的问题
B树是非叶子节点和叶子节点都会存储数据
B+树只有叶子节点才会存储数据,而且存储的数据都是在一行上,而且这些数据都是有指针指向的,也就是有顺序的
而由于他存在后面的链表,所以一般情况下,可以直接去访问链表,而不用操作判断都访问,这是索引的关键
比如你要分组,那么必然需要所有的数据,如果不是索引,那么你需要从根节点开始,一路查找,而加上了索引,那么可以直接从链表获取,所以索引会大幅度的提高效率,所以mysql才会使用B+树,而由于是链表,所以我们也会称索引是一种数据结构(链表),而由于是B+树里面的,所以我们也会称索引是B+树操作的数据结构
二叉堆:
从这里开始,基本不是以二叉查找树为主了,所以并不是小在左,大在右了,看后面图片就知道了
二叉堆本质上是一种完全二叉树,它分为两个类型:
1:大顶堆(最大堆):
最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值,即将其子节点存放比自己小的数(一般不会操作与自己相同的,但是程序是可以操作的,看你自己吧,这里无论是前面的所有类型的二叉查找树,我都是操作相同的覆盖,虽然我可以认为相同的放在左边或者右边,但这里并没有)

在这里插入图片描述

2:小顶堆(最小堆):
最小堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值

在这里插入图片描述

二叉堆的根节点叫作堆顶
最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素,最小堆的堆顶是整个堆中的最小 元素
二叉堆的存储原理:
完全二叉树也适合用数组来存储(前面说明过了),用数组来存储完全二叉树是非常节省存储空间的(虽然要连续,各有利弊,所以不是急切的,都可以使用,否则使用急切的那一个,一般的二叉堆是需要查询的,所以是急切的那一个,而不是平等的那一个,所以二叉堆就一般使用数组)
因为我们不需要 存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点

在这里插入图片描述

从图中我们可以看到,数组中下标为 i 的节点的左子节点,就是下标为 i∗2 的节点
右子节点就是下标 为 i∗2+1 的节点,父节点就是下标为 i/2 取整的节点,这里从1开始,可以基本得到具体的父节点地址,否则从0开始就需要判断了,如果是偶数需要-1,奇数不用-1,才可使得找到是父节点(所以不好找),所以我们一般从1开始
因为1开始就是4,和5,而0开始就是3,4,很明显前面的4,5除以2都是2,而3,4除以2一个是1,一个是2(那么就需要减1,才可以是1,因为4是偶数),因为本质上是除以操作省略了的原因(这里操作后面向下,而之前的是前面一个向下的,都以前面一个为主,但是程序一般直接省略,所以之前的因为后面一个因为满足整数了,所以需要减1),而乘和加以及减是基本没有的,所以除以这里需要变化
实际上只需要一点逻辑思维即可,虽然这里说明了本质(你可以选择不用看)
二叉堆的典型应用:
1:优先队列
2:利用堆求 Top K问题:
在一个包含 n 个数据的数组中,我们可以维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数 据与堆顶元素比较
如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中,然后堆自动平衡(只是对顶部平衡即可)
如果比 堆顶元素小,则不做处理,继续遍历数组
这样等数组中的数据都遍历完之后,堆中的数据就是前 K 大数据了
总体来说就是,一个大小为6的堆(里面没有数据,只是定义大小),一个大小为12的数组,我们一直往堆里添加12次,得到其中总共12个数的前6个最大的数
这样我们就相当于我们取出了一个大小为12的数组的前6个大的数据了,实际上这里的添加操作,堆是O(logn),因为也可以自平衡,实际上是因为他是完全二叉树,对于添加来说,所以才是O(logn)的(虽然是省略的情况,因为可以多出节点,即1+x的情况,前面红黑树有具体说明)
而如果我们只是操作数组,那么我们一般需要进行判断对比,或者进行移动,那么可能需要n^2(6n)的时间复杂度(你可能认为,不是6n吗,那好,我们换一句话说,如果是找到12个数里面的前12的最大的,那么自然就是n ^2了,所以实际上这个6对于12来说可是不小的了,当然,如果你还是认为是6n也没有问题,可能你认为这只是对小的排序而言,但一般都是堆好的,因为虽然这里的堆是nlogn但是n和logn的n是不同的,前者是12,后者的6,而log6是比6小的,只是这里你没有认为logn是log6而已,而只认为6n了,所以实际上堆还是好的),而使用堆,只需要nlogn即可(因为不用移动,而直接操作插入即可),因为他堆的比较,比数组自身的比较要好的多,就如二叉树与链表一样的操作的,一个是logn,一个是n,这是堆的一个应用,后面我们将会学习一个堆排序,时间复杂度也是nlogn,后面会说明
上面的都只是给出概念,如果需要学习的话,可以具体学习(网上找资源吧),因为大多数情况下是用不到的,以B+树为例子:
红黑树是因为hashmap使用到了,所以我们需要了解,但是B+树由于是mysql中使用的,所以对于我们来说,虽然也是使用到,但是并不需要了解,因为你也使用不到(而红黑树一般能直接作用与链表,所以使用的多),且具体实现可能比较困难,但若能手写出红黑树,那么大概你也能利用规则和他自带操作规则的方式(类似于红黑树的颜色反转,左右旋转等等方式),一般也能编写出来
排序:
在生活中,我们离不开排序,按大小个、按成绩等等
在计算机中也离不开排序:按编号、按价格、按远近等等

在这里插入图片描述

根据时间复杂度的不同,主流的排序算法可以分为3大类:
时间复杂度为O( )的排序算法:
冒泡排序、选择排序、插入排序、希尔排序
时间复杂度为O(nlogn)的排序算法:
快速排序 、归并排序、堆排序(前面二叉堆的应用就是这个堆排序)
时间复杂度为线性的排序算法:
计数排序、桶排序、基数排序
根据其稳定性,可以分为稳定排序和不稳定排序
稳定排序:值相同(比如数组里面的值)的元素在排序后仍然保持着排序前的顺序,注意这里说明的是顺序且值相同,而不是具体位置,比如原来5在5前面,那么排序后,5还是在5前面,他们的相对位置没有变,具体一点,就是,如果下标1的值是5,下标3的值是5,将下标1的5变到下标5中,下标3变到下标6中,且下标6的5还是下标5的5的后一个,所以可以说,判断一种排序是否稳定就是在排序过程中看相同的元素的相对位置(始终在后面)是否改变
不稳定排序:值相同(比如数组里面的值)的元素在排序后打乱了排序前的顺序(从原来的后面一个到前面一个了)
冒泡排序:
冒泡排序是最基础的排序算法
冒泡排序的英文是bubble sort,它是一种基础的交换排序
冒泡排序这种排序算法的每一个元素都可以像小气泡一样,根据自身大小,一点一点地向着数组的一侧移动
按照冒泡排序的思想,我们要把相邻的元素两两比较,当一个元素大于右侧相邻元素时,交换它们的位置
当一个元素小于或等于右侧相邻元素时,位置不变(第一轮省略了)

在这里插入图片描述

经过第一轮后:
元素9作为数列中最大的元素,就像是汽水里的小气泡一样,"漂"到了最右侧:

在这里插入图片描述

很明显每次操作都会将一个最大的到结尾,因为他们是两两互相比较的,才会选出最大
每一轮结束都会有一个元素被移到最右侧(第一轮省略了):

在这里插入图片描述

代码实现(自己可以手写一个):
package com.lagou;

/**
 *
 */
public class test1 {
    public static void main(String[] args) {
        //这个数组的数据可以非常特殊的,可以很直观的知道最少要执行多少次(因为在某些数组中,可以少次数就可以排序好,比如50和1交换位置,那么第二个循环就可以减1了或者减2,但不能减3,而第一个循环也可以减1,且甚至可能可以减的更多了,比如减2,减3,减4,但是减5就不行,具体减多少,是因为移动的原因,后面会进行解释),你随便改变一个array.length-1变成array.length-2就不会排序正确(当然,多执行,没有问题,只是不要减即可,因为多次的执行,是不会变化的,而少的执行,会导致没有排序完)
        //因为第一个循环,是需要至少刚好对应的array.length-1次才可将最小的数1放在最前面,而第二个循环,必须也是至少array.length-1次,才可将其中最大的数50放在最后面
        int[] array = {50, 2, 5, 7, 8, 3, 1};
        for (int i = 0; i < array.length - 1; i++) { //这里只需要判断需要执行多少次即可,因为每次确定一个数的原因,所以只需要确定array.length - 1次就不用确定了,可以认为是将1往前移动6次使得可以排序正确,而不是7次,因为交换中,每次都会往前移动,所以根据次数(7-1=6)只能减1
            for (int j = 0; j < array.length - 1; j++) { //这里只需要判断需要执行多少次即可,因为每次操作当前和+1的下标来操作,即两两操作,所以,最后一个下标不需要,所以是array.length - 1次,可以认为是将50往后移动6次使得可以排序正确,而不是7次,因为交换中,每次都会往后移动,所以根据次数(7-1=6)只能减1
                if (array[j] > array[j + 1]) {
                    int a = array[j + 1]; //一个if(包括其他流程控制语句,如for,while等等)语句里面定义的变量通常操作完后,会进行释放(或者本来就不同,或者不会共享,只会操作当前语句里面的,因为不同出现的if是不同的,但一般是执行完后,直接释放,当然,他说明的是流程控制语句里面定义的变量,所以如果是外面的,那么是不会释放的),所以执行才不会报错(即已定义变量的错误)
                    array[j + 1] = array[j];
                    array[j] = a;
                }
            }
        }
        for (int o = 0; o < array.length; o++) {
            System.out.println(array[o]);
        }
    }
}

很明显,上面的是稳定的排序,因为没有改变了数组里面的相同值的相对顺序(因为只有大的才会交换),当然也有改变的方式,因为你完全可以设置如果相同就交换或者说上面设置大于等于也是一样的,那么冒泡排序也可以是不稳定的(因为从原来的前面变成后面了)
冒泡排序的优化:
很明显,上面的操作是n^2(即O(n ^ 2),但通过注释解释,可以发现,实际上有些数组进行了没有必要的循环(比如上面的50和1交换位置,那么可以少一些循环)
这里给出一个优化例子(对应于前面的图片):
package com.lagou;

/**
 *
 */
public class test2 {
    public static void main(String[] args) {
        int[] nums = new int[]{5, 8, 6, 3, 9, 2, 1, 7};
        for (int i = 0; i < nums.length - 1; i++) {
            for (int j = 0; j < nums.length - 1; j++) {
                //临时变量 用于交换
                int tmp = 0;
                if (nums[j] > nums[j + 1]) {
                    tmp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = tmp;
                }
            }
        }
        for (int n : nums) {
            System.out.println(n);
        }
    }

}

外层循环优化:

在这里插入图片描述

第6轮已经可以结束了,也就是如果不需要交换了,则说明已经排好序了
思路:在外层循环处,设置标志isSort(下面的is,只要是一个标志即可),默认为排好,如果不交换则跳出本次循环
内层循环优化:
已经被移到右侧的元素不用再参与比较了
优化后的代码:
package com.lagou;

/**
 *
 */
public class test2 {
    public static void main(String[] args) {
        int[] nums = new int[]{5, 8, 6, 3, 9, 2, 1, 7};
        for (int i = 0; i < nums.length - 1; i++) {
            //默认是排好的
            boolean is = true;
            //这里设置为nums.length - 1-i,因为已经放在最后面的数据,是不用判断的,所以需要减少对应的次数,由于上面的i正好代表判断(确定)好的几个,所以就减去i
            for (int j = 0; j < nums.length - 1 - i; j++) {
                //临时变量 用于交换
                int tmp = 0;
                if (nums[j] > nums[j + 1]) {
                    //只要你交换一次,就说明没有排好,那么不确定下一次是否排好,则赋值为false
                    is = false;
                    tmp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = tmp;
                }
            }

            //如果都没有进行交换,那么说明他已经不用排了,因为下一次一定也是不用交换的,所以直接的退出
            if (is) {
                break;
            }
        }
        for (int n : nums) {
            System.out.println(n);
        }
    }

}

时间复杂度:O(n^2),虽然优化后能少点循环,但整体来说还是O(n ^ 2)的(因为时间复杂度是认为n无限大的,即操作省略了而已)
由于博客字数限制的原因,后面的知识需要在下一章博客里查看并学习(即第100章博客)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/88456.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Kubernetes】一主二从环境搭建,详细的图文描述

kubernetes&#xff0c;是一个全新的基于容器技术的分布式架构领先方案&#xff0c;是谷歌严格保密十几年的秘密武器----Borg系统的一个开源版本&#xff0c;于2014年9月发布第一个版本&#xff0c;2015年7月发布第一个正式版本。 kubernetes的本质是一组服务器集群&#xff0…

使用Java API操作HDFS

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录学习目标&#xff08;一&#xff09;了解HDFS Java API1、HDFS常见类与接口2、FileSystem的常用方法&#xff08;二&#xff09;编写Java程序访问HDFS1、创建Maven项…

Nacos 配置中心之长轮询--服务端

先回顾一下客户端和服务端交互的过程 服务端 入口 直接看长轮询的接口 ConfigController.listener PostMapping("/listener")Secured(action ActionTypes.READ, parser ConfigResourceParser.class)public void listener(HttpServletRequest request, HttpServ…

抓住三个关键因素,提高你的ASA广告效果!

​ 众所周知&#xff0c;App Store 作为 iOS 端的流量收口&#xff0c;旗下的 ASA 广告更是广告主在 iOS 生态投放广告的唯一渠道&#xff0c;所提供的四大广告位&#xff08;Today 标签、搜索标签、搜索结果和产品页面&#xff09;覆盖了用户访问的全路径&#xff0c;为广告主…

12月14日:跟着猫叔写代码api中的增删改查

首先在数据库中建立一个学生成绩信息表 DROP TABLE IF EXISTS bro_ceshiapi; CREATE TABLE bro_ceshiapi (id int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT id,name varchar(100) DEFAULT NULL COMMENT 姓名,class varchar(100) DEFAULT NULL COMMENT 班级,score decima…

[附源码]Python计算机毕业设计Django基于vuejs的文创产品销售平台app

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

学习Vue3 - 认识 Reactive 全家桶

reactive 用来绑定复杂的数据类型&#xff0c;例如&#xff1a;对象、数组 reactive 源码约束了我们的类型 他是不可以绑定普通的数据类型的&#xff0c;这样是不允许的&#xff0c;会报错 因此&#xff0c;如果绑定普通的数据类型&#xff0c;可以使用ref ref绑定对象或者…

计算机SCI论文,如何写吸引人的摘要? - 易智编译EaseEditing

摘要简明扼要的概括全文的主要内容&#xff0c;是整篇文章的精华&#xff0c;是编辑、审稿专家以及读者阅读文章的最先关注的部分。 一个好的摘要可以正确反映文章内容&#xff0c;引起编辑、审稿专家以及读者的关注。那如何写出一个好的论文摘要呢&#xff0c;今天小易为大家…

一种基于摩斯密码的页面加密方法(web和小程序)

1. 开始 web开发中&#xff0c;常有一些功能仅希望对开发、测试人员等一小部分人展示&#xff0c;比如测试一个小程序项目中&#xff0c;想让测试人员快速复制当前对应的h5页面&#xff0c;这时候如果页面是必须登录的&#xff0c;我们可以借助vconsole&#xff0c;然后维护一…

redis之主从切换可能有哪些问题

写在前面 本文一起看下Redis cluster 集群模式下&#xff0c;发生了主从切换时可能存在的问题以及应对方案。 1&#xff1a;主从数据不一致 主从数据不一致&#xff0c;是由于主从同步延迟造成的&#xff0c;可能的解决方案如下&#xff1a; 1&#xff1a;尽量将主从同机房…

React面试:谈谈虚拟DOM,Diff算法与Key机制

1.虚拟dom 原生的JS DOM操作非常消耗性能&#xff0c;而React把真实原生JS DOM转换成了JavaScript对象。这就是虚拟Dom&#xff08;Virtual Dom&#xff09; 每次数据更新后&#xff0c;重新计算虚拟Dom&#xff0c;并和上一次生成的虚拟dom进行对比&#xff0c;对发生变化的…

Ansys Zemax | 用于数字投影光学中均匀照明的蝇眼阵列

简介 在数字投影仪设计中&#xff0c;我们希望确保数字光源与投影图像在辐照度分布相匹配。因此&#xff0c;这一约束要求投影仪设计包含均匀照明的空间光调制器——通常以LCD面板的形式呈现。理论上听起来很容易&#xff0c;但实际上&#xff0c;此面板上的光源光束通常是高斯…

语音输入转文字怎么操作?分享几种语音转文字技巧

相信有不少小伙伴在整理语音文件的时候&#xff0c;都会有过怎样把这些语音直接转换成文字的想法吧。每次在我开完会之后&#xff0c;需要对会议语音进行整理时&#xff0c;都会产生这种想法。因为我们需要不断的去听这个会议的语音内容&#xff0c;这样做既费时又费力。但其实…

MATLAB生成2D和3D格网(GUI程序)

目录 一、写函数DataStructure_Fnc 二、控件属性 三、生成2D格网代码 三、生成3D格网代码 一、写函数DataStructure_Fnc 函数代码&#xff0c;生成三角网需要调用此函数 function DataStructureDataStructure_Fnc(Table) [row col]size(Table); Table(1:end,5:7)-1; for j1…

【配置指导】如何配置dataFEED edgeConnector Siemens以实现西门子PLC与阿里云之间的双向通信

本配置指导手册介绍了如何配置dataFEED edgeConnector Siemens&#xff0c;以通过MQTT来将西门子S7-1200 PLC数据上传到阿里云&#xff1b;以及从阿里云发布数据&#xff0c;并传输到PLC中&#xff0c;从而实现西门子S7-1200 PLC与阿里云之间的双向通信。 主要内容包括&#xf…

30-Vue之ECharts-直角坐标系的常用配置

直角坐标系的常用配置前言直角坐标系常用配置网格坐标轴区域缩放前言 本篇来学习下直角坐标系的常用配置 直角坐标系 直角坐标系的图表指的是带有x轴和y轴的图表, 常见的直角坐标系的图表有: 柱状图 折线图 散点图 常用配置 网格 grid&#xff1a;是用来控制直角坐标系的…

可落地的、不基于框架的分布式事务解决方案

两阶段提交 2PC 在MySQL InnoDB中&#xff0c;为了保证Bin Log和Redo Log的一致性&#xff0c;便采用了两阶段提交&#xff1b;ZooKeeper、ETCD集群为了保证数据一致性&#xff0c;也采用了两阶段提交&#xff0c;RocketMQ的事务消息也采用了两阶段提交&#xff0c;可见两阶段…

从VirtualBox换成KVM虚拟机管理程序?

好消息是&#xff0c;您可以轻松地将VDI格式的VirtualBox VM迁移到qcow2(即KVM的磁盘映像格式)&#xff0c;不用创建新的KVM来宾计算机。 我们在本文中将概述如何将VirtualBox VM迁移到Linux中KVM VM的逐步过程。 第一步&#xff1a;列出现有的VirtualBox映像 首先&#xff0c…

泰斯公式Thiem’s equation地下水

基本形式 泰斯公式1描述了在含水层抽水时的地下水流动。 多井作业时非承压含水层的方程形式如下 H(s)和H0(s)分别表示s点的估计地下水位和初始地下水位&#xff0c;K表示水力导率&#xff0c;ri表示预测位置与贡献井i之间的距离&#xff0c;n是贡献井的集合&#xff0c;Q表…

Win11 21H2 12月最新更新了哪些内容?

微软今天发布了12月最新的累积更新补丁&#xff0c;用户可以升级KB5021234将版本号提升至 build 22000.1335&#xff0c;并解决了远程网络问题以及可能影响数据保护应用程序编程接口 &#xff08;DPAPI&#xff09; 解密的问题。此外&#xff0c;该更新还包括之前在 11 月 15 日…