数据结构与算法
数据结构和算法,一个非常古老的课题,工作的时候,一般只求程序能跑,并不太关注性能
一般情况下,我们尽量避坑,即避免这样: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 = 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< 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 ;
int i= 1 ;
for ( ; i<= n; i++ ) {
s= s+ i;
}
return s;
}
所以我们假设执行一行代码的时间为t,通过估算(即认为每一行代码都是t,所以只考虑估算,而不考虑有多长的代码)
很明显代码的执行时间T(n)与执行次数成正比,记做:
T ( n) = O ( f ( n) )
再次举例:
int sum ( int n) {
int s= 0 ;
int i= 1 ;
int j= 1 ;
for ( ; i<= n; i++ ) {
j= 1 ;
for ( ; j<= n; j++ ) {
s= s+ i+ j;
}
}
return s;
}
计算时间复杂度的技巧:
计算循环执行次数最多的代码
总复杂度=量级最大的复杂度,也就是之前说的:我们通常只会看循环或者说(及其)变化大(通常是最大,主要是这个)的代码来进行估算(时间复杂度)
比如把上面两段代码合在一起:
int sum ( int n) {
int s= 0 ;
int i= 1 ;
int j= 1 ;
for ( ; i<= n; i++ ) {
s= s+ i;
}
for ( ; i<= n; i++ ) {
j= 1 ;
for ( ; j<= m; j++ ) {
s= s+ i+ j;
}
}
return s;
}
常见的时间复杂度:
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 ;
}
快速排序、归并排序的时间复杂度都是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;
}
O(m*n):
int sum ( int m, int n) {
int s= 0 ;
int i= 1 ;
int j= 1 ;
for ( ; i<= m; i++ ) {
j= 1 ;
for ( ; j<= n; j++ ) {
s= s+ i+ j;
}
}
return s;
}
以后我们操作的时间复杂度,默认为是操作无限大的,所以该省略的就省略,来表示时间复杂度
空间复杂度:
空间复杂度全称是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系
比如将一个数组拷贝到另一个数组中,就是相当于空间扩大了一倍(由原来的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
该公式解释了三个方面:
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
中间插入(包含首部插入,因为是连续的,所以首部实际上也算中间):
在数据的实际元素数量小于数组长度的情况下:
由于数组的每一个元素都有其固定下标,所以首先把插入位置及后面的元素向后移动,腾出地方(有时候是最后的一个数先移动,否则可能会被覆盖的,或者使用第三方数来保存,那么可以让插入的地方先移动,我们通常使用第三方数来操作,因为正向的循环对大多数人比较友好,即从小到大的加,而不是从大到小的减,这是大多数人的习惯,除了某些习惯从大到小的人群)
上面不同的方式,也是算法的一种体现
再把要插入的元素放到对应的数组位置上
超范围插入:
假如现在有一个数组,已经装满了元素,这时还想插入一个新元素,或者插入位置是越界的
这时就要对原数组进行扩容:可以创建一个新数组,长度是旧数组的2倍
再把旧数组中的元素统统复制过去,这样就实现了数组的扩容
int [ ] numsNew= new int [ nums. length* 2 ] ;
System . arraycopy ( nums, 0 , numsNew, 0 , nums. length) ;
nums= numsNew;
删除元素:
数组的删除操作和插入操作的过程相反,如果删除的元素位于数组中间,其后的元素都需要向前挪动1位
for ( int i= p; i< nums. length; i++ ) {
nums[ i- 1 ] = nums[ i] ;
}
时间复杂度:
读取(单纯的读取,而不是全部读取,即查询,这里之所以也是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
现在,你自己要求自己写出对应数组的增删改查的方法,并测试
这里给出对应数组的全部基本操作代码(记得自己写一下,然后再看哦):
public class Array {
int array = 8 ;
int [ ] nums = new int [ array] ;
public Array ( ) {
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 ;
}
return nums[ i] ;
}
public void update ( int i, int n) {
if ( i < 0 || i >= array) {
System . out. println ( "下标越界了" ) ;
return ;
}
nums[ i] = n;
}
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 ) ;
d. insert ( 7 , 3 ) ;
d. insert ( 8 , 6 ) ;
d. select ( ) ;
d. reverseSelect ( ) ;
System . out. println ( d. get ( 1 ) ) ;
d. delete ( 6 ) ;
d. update ( 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 {
Node head;
public class Node {
int id;
String name;
Node next;
public Node ( int id, String name) {
this . id = id;
this . name = name;
}
}
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;
return ;
}
}
if ( temp1. id > node. id) {
node. next = temp1;
if ( temp1 != head) {
temp2. next = node;
return ;
}
head = node;
return ;
}
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 ;
}
temp2. next = node;
return ;
}
}
}
head = node;
}
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 ( "没有节点删除了" ) ;
}
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 ( "没有节点更新了" ) ;
}
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 ( ) {
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 ( ) ;
t. delete ( 2 ) ;
t. select ( ) ;
t. reverseSelect ( ) ;
t. update ( 26 , "5" ) ;
t. select ( ) ;
t. reverseSelect ( ) ;
t. selectByNode ( 2 ) ;
t. selectByNode ( 26 ) ;
}
}
至此,单(向)链表操作完毕,接下来我们来完成双向链表(记得自己写一下,然后再看哦):
public class test1 {
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;
}
}
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;
}
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 ( "没有节点删除了" ) ;
}
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 ( "没有节点修改了" ) ;
}
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 ( ) ;
t. delete ( 2 ) ;
t. select ( ) ;
t. reverseSelect ( ) ;
t. update ( 26 , "5" ) ;
t. select ( ) ;
t. reverseSelect ( ) ;
t. selectByNode ( 2 ) ;
t. selectByNode ( 26 ) ;
}
}
至此,双向链表操作完毕,接下来我们来完成循环链表(记得自己写一下,然后再看哦):
public class test2 {
Node head;
public class Node {
int id;
String name;
Node next;
public Node ( int id, String name) {
this . id = id;
this . name = name;
}
}
public Node last ( ) {
Node temp1 = head;
while ( true ) {
temp1 = temp1. next;
if ( temp1. next == head) {
return temp1;
}
}
}
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 ;
}
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;
}
}
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 ( "没有删除的节点了" ) ;
}
}
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 ( "没有修改的节点了" ) ;
}
}
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 ( ) ;
t. delete ( 2 ) ;
t. select ( ) ;
t. reverseSelect ( ) ;
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)了
链表:优缺点
优势:插入、删除、更新效率高,省空间(因为不用扩容,即省下了扩容多余的空间,当然,扩容一般都是一次性是扩大很多的,这是为了防止多次的扩容,所以相对于这样的整体来说,的确是省很多空间的)
劣势:查询效率较低,不能随机访问
应用 :
链表的应用也非常广泛,比如树、图、Redis的列表、LRU算法实现、消息队列等
数组与链表的对比 :
数据结构没有绝对的好与坏,数组和链表各有千秋,就如算法一样,也是只有好和坏,因为他们都代表程序的一部分,他们都有同样的好与坏,在生活中我们也经常遇到这种,比如电脑的硬件配置,可能各有对应的优点
那么有个疑问,我们之前说过,时间复杂度是估算的,那么我们必须看操作的时间复杂度吗
实际上并不是,在某些时候,我们需要看他全部的操作,所以可能对应的具体操作的时间复杂度是小的
但是通常实际上操作时,可能是某些结合,所以时间复杂度低的实际上未必就好,比如链表的插入,必然需要查找
所以在某些情况下,我们也会认为链表的插入是O(n),这种情况则是根据具体方法来描述的
所以可能有些博客,说链表的插入操作是O(n)的时间复杂度(以后我们就用类似的O(n)来表示时间复杂度了,这5个字就不写了),也是有一定的道理的,但不是具体操作,而是该整体方法,才能这样说明
但是我们可以知道,链表的插入,首先会根据指针一路找过去的,然后进行指针赋值
而数组的插入先进行移动,然后赋值,我们可以发现,他们基本类似,但是数组的长度需要考虑(前面的内存存储中可以很明显的知道该链表在内存上比数组要好,也有具体的描述:“这样可以灵活有效地利用零散的碎片空间,这也是不使用数组,而使用链表的好处之一”)
所以在某些方面来说,链表在插入方面比较友好(省空间,不用扩容),但是可能在某些方面并不好
因为我们知道他只能从头节点开始,也就是说,他的优化的上限太低了
而数组,在某些时候,可以更好的利用某些算法来进行优化,比如我们直接的将数组进行二分(是分成两个数组,即基本不是后面的二分法),然后用两个线程来查找,即二分多线程(只针对数据的查找来进行优化,而不针对资源,这里这里是不考虑资源的,那么的确是优化了,如果考虑资源,那么多出一个线程虽然查找是快了,但是也消耗了更多的资源,所以在这种情况下不见得是优化的)
而链表因为链表地址的原因,所以基本很难这样使用线程操作,因为地址只能是一个,所以通常只能一步一步的操作,这也是之前我说的
为什么在下游中,数据结构的优化是有限的原因(不同的数据结构的上限不同,数组的上限一般都很大,链表一般很小)
虽然他的优化上限低,但是由于链表省空间,所以在下游中,他的使用还是很多的,上游有时也会使用
简单来说,在不考虑其他情况下(如优化),他们的优势如下:
数组的优势在于能够快速定位元素,对于读操作多、写操作少的场景来说,用数组更合适一些
链表的优势在于能够灵活地进行插入和删除操作而不用考虑内存空间的问题(数组需要扩容)
如果需要在尾部频繁插入、删除元素,用链表更合适一些(因为数组在尾部插入通常需要扩容)
数组和链表是线性数据存储的物理存储结构:即顺序存储和链式存储(前面已经说明了,这也是他们各自优势的本质)
从而数组可以直接用下标获取数据,而链表不要考虑内存存储空间
栈:
栈和队列都属于线性数据的逻辑存储结构(逻辑存储结构:我们认为他要怎么存放,就怎么存放,比如我们把大的放在前面,小的放在后面,这样也算逻辑存储结构,只是他们分线性和非线性的存储,这个从大到小,算是线性,也就是我们说的线性表和非线性表,前面有说明,如数组和链表,那里说明的就是逻辑存储结构,而不说物理存储结构,虽然他们本身就是物理结构的方式,但以逻辑结构而言,他们都是线性结构的)
由于他们是逻辑存储结构,所以通常需要物理结构(顺序存储结构和链式存储结构)来实现他们的具体效果,这里需要注意
实际上无论是线性结构还是非线性结构都需要物理结构来操作,比如数组和链表(虽然他们在逻辑上也称为线性结构)
即逻辑结构是:看起来是线性还是非线性
物理结构是:线性结构和非线性结构的实现方式
比如:数组看起来是线性结构,且使用他的物理结构方式是数组(顺序存储结构,而不是链式存储结构)
上述我们只是给数据结构进行某种定义,大多数情况下,这些只是专业术语,用来表示他们的形状的,我们并不需要过于理会
概念:
栈(stack)是一种线性数据结构,栈中的元素只能先入后出(First In Last Out,简称FILO)
最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶 (top)
存储原理:
栈既可以用数组来实现,也可以用链表来实现
栈的数组实现如下:
数组实现的栈也叫顺序栈(或静态栈)
栈的链表实现如下:
链表实现的栈也叫做链式栈(或动态栈,之所以不是静态栈,是因为不用扩容):
操作:
入栈(压栈):
入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶
出栈(弹栈):
出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会成为新的栈顶
接下来给出数组方式的栈实现的全部基本操作代码(记得自己写一下,然后再看哦):
public class test3 {
int [ ] array;
int count;
int max = 5 ;
public test3 ( int n) {
array = new int [ n] ;
count = 0 ;
}
public void push ( int i) {
if ( count + 1 <= max) {
array[ count] = i;
count++ ;
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 ;
}
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 ( "没有节点信息了" ) ;
}
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 ( ) ;
}
}
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 ;
}
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 ( ) ;
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;
}
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;
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 ( 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 ;
}
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 ;
}
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),他们的意思是针对与他们自身的操作(单纯的赋值,如果是后面的方式,那么就都是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为例:
index = HashCode ( Key ) % Array . length
int index= Math . abs ( "Hello" . hashCode ( ) ) % 10 ;
这基本是最简单的计算方式
还有很多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 {
ListEntry [ ] entry = new ListEntry [ 8 ] ;
int size;
double top = 0.75 ;
public class Entry {
String key;
String value;
Entry next;
public Entry ( String key, String value) {
this . key = key;
this . value = value;
}
}
public class ListEntry {
Entry head;
public void addEntry ( Entry entry) {
Entry temp1 = head;
while ( true ) {
if ( temp1. key. equals ( entry. key) ) {
temp1. value = entry. value;
return ;
}
if ( temp1. next == null ) {
temp1. next = entry;
return ;
}
temp1 = temp1. next;
}
}
public void deleteEntry ( String key) {
Entry temp1 = head;
Entry temp2 = head;
while ( true ) {
temp2 = temp1;
temp1 = temp1. next;
if ( temp1 == null ) {
System . out. println ( "没有该节点" ) ;
return ;
}
if ( temp1. key. equals ( key) ) {
temp2. next = temp1. next;
return ;
}
}
}
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;
}
}
public String getEntry ( String key) {
Entry temp1 = head;
while ( true ) {
if ( temp1 == null ) {
System . out. println ( "没有该节点" ) ;
return null ;
}
if ( temp1. key. equals ( key) ) {
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 ] ;
size = 0 ;
for ( int i = 0 ; i < entry. length; i++ ) {
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 {
numsNew[ index] . addEntry ( new Entry ( temp1. key, temp1. value) ) ;
}
temp1 = temp1. next;
} else {
break ;
}
}
}
}
entry = numsNew;
}
public void put ( String key, String value) {
int index = Math . abs ( key. hashCode ( ) % entry. length) ;
if ( entry[ index] == null ) {
ListEntry entry1 = new ListEntry ( ) ;
entry[ index] = entry1;
entry[ index] . head = new Entry ( key, value) ;
size++ ;
if ( size >= entry. length * top) {
resize ( ) ;
}
return ;
}
entry[ index] . addEntry ( new Entry ( 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 ;
}
String value = this . entry[ index] . getEntry ( key) ;
return 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 ;
}
this . entry[ index] . updateEntry ( key, value) ;
}
public void delete ( String key) {
int index = Math . abs ( key. hashCode ( ) % entry. length) ;
if ( entry[ index] == null ) {
System . out. println ( "没有该key指定的value数据" ) ;
return ;
}
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 ;
}
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 ( ) ;
}
}
}
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) ;
}
}
时间复杂度:
写操作: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++ ) {
System . out. println ( "Hello World" ) ;
}
}
public void recursion ( int n) {
if ( n <= 0 ) {
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 ) ;
}
public static void main ( String [ ] args) {
test t = new test ( ) ;
int fa = t. fa ( 6 ) ;
System . out. println ( fa) ;
}
}
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) ) ;
}
public static void main ( String [ ] args) {
test1 t = new test1 ( ) ;
System . out. println ( t. fa ( 6 ) ) ;
System . out. println ( Math . round ( t. fa ( 6 ) ) ) ;
}
}
11 / 2 = 5
5 / 2 = 2
2 / 2 = 1
1 / 2 = 0
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
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
0.5 / 2 = 0.2
0.2 / 2 = 0.1
0.1 / 2 = 0.05
. . . . . .
0.75 * 2 = 1.5
0.5 * 2 = 1.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 ;
int last = i. length - 1 ;
int h = ( head + last) / 2 ;
while ( true ) {
if ( i[ h] == j) {
return h;
}
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 ) ) ;
}
}
至此,我们使用二分法查找数字操作完毕,接下来我们使用递归来操作
使用递归来操作:
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 ) ;
}
}
public static void main ( String [ ] args) {
int [ ] nums = { 3 , 12 , 24 , 31 , 46 , 48 , 52 , 66 , 69 , 79 , 82 } ;
System . out. println ( fb ( nums, 0 , nums. length - 1 , 66 ) ) ;
}
}
经典案例:
一个有序数组有一个数出现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 ) {
if ( h == 0 || h == array. length - 1 ) {
return h;
}
if ( array[ h] == array[ h + 1 ] ) {
head = h + 2 ;
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;
}
}
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) ) ;
}
}
时间复杂度:
时间复杂度就是 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 ;
}
}
}
}
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) {
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 ) ;
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) ;
TreeNode node = tree. getNode ( 7 ) ;
if ( node != null ) {
System . out. println ( "数值为:" + node. data) ;
} else {
System . out. println ( "没有节点,也就没有数值" ) ;
}
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( ) ;
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;
}
}
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 [ ] in = new int [ max] ;
int i = 0 ;
TreeNode [ ] a = new TreeNode [ max] ;
int aa = 0 ;
TreeNode treeNode = root;
if ( treeNode == null ) {
System . out. println ( "[]" ) ;
return ;
}
while ( true ) {
if ( treeNode == null && aa == 0 ) {
break ;
}
while ( true ) {
if ( treeNode == null ) {
break ;
}
in[ i] = treeNode. data;
i++ ;
a[ aa] = treeNode;
aa++ ;
treeNode = treeNode. left;
}
treeNode = a[ -- aa] . right;
a[ aa] = 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] ;
int aa = 0 ;
TreeNode treeNode = root;
if ( treeNode == null ) {
System . out. println ( "[]" ) ;
return ;
}
while ( true ) {
if ( treeNode == null && aa == 0 ) {
break ;
}
while ( true ) {
if ( treeNode == null ) {
break ;
}
a[ aa] = treeNode;
aa++ ;
treeNode = treeNode. left;
}
treeNode = a[ -- aa] ;
in[ i] = treeNode. data;
i++ ;
treeNode = a[ aa] . right;
a[ aa] = 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] ;
int aa = 0 ;
TreeNode treeNode = root;
if ( treeNode == null ) {
System . out. println ( "[]" ) ;
return ;
}
TreeNode pre = null ;
int p = 0 ;
while ( true ) {
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;
}
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 ;
} 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 ) ;
System . out. println ( "=============前序遍历是:=============" ) ;
tree. preSelect ( ) ;
System . out. println ( "=============中序遍历是:=============" ) ;
tree. inSelect ( ) ;
System . out. println ( "=============后序遍历是:=============" ) ;
tree. latSelect ( ) ;
System . out. println ( "总个数是:" + tree. max) ;
TreeNode node = tree. getNode ( 7 ) ;
if ( node != null ) {
System . out. println ( "数值为:" + node. data) ;
} else {
System . out. println ( "没有节点,也就没有数值" ) ;
}
tree. delete ( 7 ) ;
System . out. println ( "=============前序遍历是:=============" ) ;
tree. preSelect ( ) ;
System . out. println ( "=============中序遍历是:=============" ) ;
tree. inSelect ( ) ;
System . out. println ( "=============后序遍历是:=============" ) ;
tree. latSelect ( ) ;
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;
}
}
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 ;
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 ) ;
System . out. println ( "=============前序遍历是:=============" ) ;
tree. preSelect ( ) ;
System . out. println ( "=============中序遍历是:=============" ) ;
tree. inSelect ( ) ;
System . out. println ( "=============后序遍历是:=============" ) ;
tree. latSelect ( ) ;
System . out. println ( "总个数是:" + tree. max) ;
TreeNode node = tree. getNode ( 7 ) ;
if ( node != null ) {
System . out. println ( "数值为:" + node. data) ;
} else {
System . out. println ( "没有节点,也就没有数值" ) ;
}
tree. delete ( 7 ) ;
System . out. println ( "=============前序遍历是:=============" ) ;
tree. preSelect ( ) ;
System . out. println ( "=============中序遍历是:=============" ) ;
tree. inSelect ( ) ;
System . out. println ( "=============后序遍历是:=============" ) ;
tree. latSelect ( ) ;
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) ;
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 ) ;
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;
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 ;
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 ;
}
}
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 grandparent = node. parent. parent;
if ( grandparent. left == node. parent) {
if ( grandparent. right != null && grandparent. right. color == false ) {
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 ) {
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;
if ( node1!= null ) {
node1. parent = node. parent;
}
grandparent. left = node;
node. parent = grandparent;
return ;
}
if ( node. parent. right == node && grandparent. right == node. parent) {
root = node. parent;
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;
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 ) {
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;
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 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 = 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 ( ) ;
}
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) ;
}
private void banlanceInsert ( RBTreeNode node) {
RBTreeNode father, grandFather;
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) ;
}
private void leftRotate ( RBTreeNode node) {
RBTreeNode right = node. getRight ( ) ;
RBTreeNode parent = node. getParent ( ) ;
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) ;
}
right. setLeft ( 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;
static final int TREEIFY_THRESHOLD = 8 ;
至此红黑树大致说明完毕,虽然前面我自己编写了一个代码,但是由于功能实现,那么也行,虽然可能有隐患(测试没有问题,所以实际上可以认为没有隐患),但是自己写的代码有助于理解红黑树的程序操作,你也自己来手写一个吧
多路查找树(也称多路树,注意:他只是在普通的二叉树的另一种而已,而没有其他的限制,比如二叉查找树的限制,即虽然他也有查找两个字,但是多路树和多路查找树是等价的,而不是像二叉树包含二叉查找树一样的包含关系):
多路查找树(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) {
int [ ] array = { 50 , 2 , 5 , 7 , 8 , 3 , 1 } ;
for ( int i = 0 ; i < array. length - 1 ; i++ ) {
for ( int j = 0 ; j < array. length - 1 ; j++ ) {
if ( array[ j] > array[ j + 1 ] ) {
int a = array[ j + 1 ] ;
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 ;
for ( int j = 0 ; j < nums. length - 1 - i; j++ ) {
int tmp = 0 ;
if ( nums[ j] > nums[ j + 1 ] ) {
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章博客)