java单链集合_Java集合-数据结构之栈、队列、数组、链表和红黑树

数据结构部分,复习栈,队列,数组,链表和红黑树,参考博客和资料学习后记录到这里方便以后查看,感谢被引用的博主。

栈(stack)又称为堆栈,是线性表,它只能从栈顶进入和取出元素,有先进后出,后进先出(LIFO, last in first out)的原则,并且不允许在除了栈顶以外任何位置进行添加、查找和删除等操作。栈就相当如手枪的弹夹,先进入栈的数据被压入栈底(bottom),而后入栈的数据存放在栈顶(top)。当需要出栈时,是先让栈顶的数据出去后,下面的数据才能出去,这就是先进后出的特点。插入数据一般称为进栈或压栈(push),删除数据则称为出栈或弹栈(pop)。

下面参考博文,地址:https://www.cnblogs.com/ysocean/p/7911910.html,底层使用数组来模拟一个栈的功能,具有push,pop,peek等常用方法,原生的stack是继承自vector类的子类,其具备父类的所有方法,这里模拟除了前面三种方法外,还写了判断自定义栈是否为空,以判断自定义栈是否满等方法。

自定义栈,底层采用数组模拟

1 packagedataStructure;2 /**

3 * 自定义栈,使用数组来实现4 */

5 public classMyStack {6

7 private int size;//数组大小

8 private String[] arr;//数组

9 private int top=-1;//默认栈顶位置10

11 //构造方法

12 public MyStack(intsize) {13 this.size =size;14 arr=newString[size];15 }16

17 //压栈

18 public voidpush(String value){19 //top范围0到size-1

20 if(top<=size-2){21 arr[++top]=value;22 }23 }24

25 //出栈

26 publicString pop(){27 //原栈顶元素设置为null,等待gc自动回收

28 return arr[top--];29 }30

31 //查看栈顶

32 publicString peek(){33 if(top>-1){34 returnarr[top];35 }else{36 return null;37 }38 }39

40 //检查栈是否为空

41 public booleanEmpty(){42 return top<0;43 }44

45 //检查栈是否满

46 public booleanFull(){47 return top==size-1;48 }49

50 //检查栈中元素数量

51 publicString size(){52 int count=top+1;53 return "栈中元素:"+count+" | 栈容量"+size;54 }55

56 }

测试代码,验证自定义栈中的方法。

1 packagedataStructure;2

3 public classTestMyStack {4

5 public static voidmain(String[] args) {6 //测试自定义Stack

7 MyStack stack=new MyStack(3);8 //压入栈顶

9 stack.push("Messi");10 stack.push("Ronald");11 stack.push("Herry");12

13 //查看栈中元素数量

14 System.out.println(stack.size());15

16 //查看栈顶元素

17 System.out.println(stack.peek());18

19 //循环遍历栈中元素

20 while(!stack.Empty()){21 System.out.println(stack.pop());22 }23

24 //判断栈是否为空

25 System.out.println(stack.Empty()); //true

26 System.out.println(stack.size());27

28 }29

30 }

控制台输出情况。

64bece806bf4c97e93feabe295fef5f1.png

自定义一个栈,实现数组自动扩容并能储存不同的数据类型

在上面例子的基础上,依然参考上述博文,自定义一个栈并能实现栈容量自动扩容,以及栈中可以存储不同的数据类型。

1 packagedataStructure;2

3 importjava.util.Arrays;4 importjava.util.EmptyStackException;5

6 /**

7 * 自定义栈,使用数组来实现,可以实现数组自动扩容,以及存储不同的数据类型8 */

9 public classMyArrayStack {10 //定义属性

11 private int size;//容量

12 private Object[] arr;//对象数组

13 private int top;//栈顶位置14

15 //默认构造方法

16 publicMyArrayStack() {17 this.size=10;18 this.arr=new Object[10];19 this.top=-1;20 }21

22 //自定义数组容量的构造方法

23 public MyArrayStack(intsize) {24 if(size<0){25 throw new IllegalArgumentException("栈容量不能小于0"+size);26 }27 this.size =size;28 this.arr=newObject[size];29 this.top=-1;30 }31

32 //压栈

33 publicObject push(Object value){34 //压栈之前欠判断数组容量是否足够,不够就扩容

35 getNewCapacity(top+1);36 arr[++top]=value;37 returnvalue;38 }39

40 //出栈

41 publicObject pop(){42 if(top==-1){43 throw newEmptyStackException();44 }45 Object obj=arr[top];46 //删除原来栈顶的位置,默认设置为null,等待gc自动回收

47 arr[top--]=null;48 returnobj;49 }50

51 //查找栈顶的元素

52 publicObject peek(){53 returnarr[top];54 }55

56 //判断栈是否为空

57 public booleanEmpty(){58 return top==-1;59 }60

61 //检查栈中元素

62 publicString size(){63 int count=top+1;64 return "栈中元素:"+count+" | 栈容量"+size;65 }66

67 //返回栈顶到数组末端内容

68 public voidprintWaitPosition(){69 if(top

78

79 //写一个方法判断数组是否需要自动扩容

80 public boolean getNewCapacity(intindex){81 //判断压入后的数组下标,是否超过了数组容量限制,超出就扩容

82 if(index>size-1){83 //扩容2倍

84 int newSize=0;85 if((size<<1)-Integer.MAX_VALUE>0){86 newSize=Integer.MAX_VALUE;87 }else{88 newSize=size<<1;89 }90 //数组扩容后复制原数组数据,扩容的部分,默认为Object初始值

91 this.size=newSize;92 arr=Arrays.copyOf(arr, newSize);93 return true;94 }else{95 return false;96 }97 }98 }

测试代码,验证自动扩容,空闲位置是什么。

1 packagedataStructure;2

3 public classTestMyArrayStack {4

5 public static voidmain(String[] args) {6 //测试自定义MyArrayStack

7 MyArrayStack stack=new MyArrayStack(2);8 stack.push("Messi");9 stack.push("Ronald");10 System.out.println(stack.size());11 System.out.println(stack.peek());12 //超出容量后继续压栈

13 stack.push("boy you will have a good future");14 System.out.println(stack.size());15 System.out.println(stack.peek());16 //打印stack中空闲位置内容

17 stack.printWaitPosition();18 //压入数字

19 stack.push(8848);20 System.out.println(stack.size());21 System.out.println(stack.peek());22 stack.printWaitPosition();23 //压入布尔类型

24 stack.push(true);25 System.out.println(stack.size());26 System.out.println(stack.peek());27 stack.printWaitPosition();28 }29 }

控制台输出情况。

4b28eac60e6ba2db66a1c94983100b68.png

在参考博文中,栈除了以上用途外,还可以巧妙用在将字符串反转,还有验证分隔符是否匹配,以后如果有需要可以参考引用的博文。

队列

队列(queue),跟堆栈类似,也是线性表,它是仅允许在尾部(tail)进行插入,在头部(head)进行删除,满足先进先出(FIFO)的原则,类似火车头进入山洞,先进入山洞的车厢就先出来山洞,后进入山洞的火车头后出来山洞。查看队列源码,可以看到接口有如下方法。

25be7a0456479bef87fd56db19293bae.png

简单的整理一下如下。

(1)插入元素到tail尾部:add(e),offer(e),前者为执行失败时抛出异常,后者不会抛出但返回特殊值(null或false)。

(2)移除head头部元素:remove(),poll(),前者为执行失败时抛出异常,后者不会抛出但返回特殊值(null或false)。

(3)查看列头head元素:element(),peek(),前者为执行失败时抛出异常,后者不会抛出但返回特殊值(null或false)。

下面分别使用两种类型的方法进行queue操作。

使用会抛出异常的方法

1 packageDataCompose;2

3 importjava.util.LinkedList;4 importjava.util.PriorityQueue;5 importjava.util.Queue;6

7 /**

8 * 测试队列,队列Queue具有先进先出的特点9 */

10 public classQueueTest {11

12 public static voidmain(String[] args) {13 //创建一个队列,使用LinkedList来创建对象,并被接口指向

14 Queue queue=new LinkedList();15 //Queue queue=new PriorityQueue();16 //使用会抛出异常的方法,添加元素到尾部,删除头部元素,以及查看头部元素操作17

18 //1 添加元素 add方法

19 queue.add("Messi");20 queue.add("Herry");21 queue.add(null);22 System.out.println(queue);23

24 //2 删除头部元素 remove方法

25 String str=queue.remove();26 System.out.println(str);27 System.out.println(queue);28

29 //3 查看头部元素 element方法

30 System.out.println(queue.element());31

32 }33 }

控制台输出结果,可以看出如果实现类为LinkedList时可以插入null,并且看出先加入的Messi,如果执行remove方法后也是先移除,执行element方法也是先查询得到头部元素,因此遵循先进先出原则。

4cbf59ad32ba67fed01977989850e82e.png

如果不往集合中add元素,直接执行remove方法会发生如下报错,提示没有元素异常,并发现执行remove方法,会执行LinkedList底层的removeFirst方法,说明其移除的就是第一个元素。

378947eb4afb844268427fbe03ad1d31.png

同样如果不往集合中添加元素,直接执行element方法会报如下错,也提示没有元素异常,并发现执行element方法时会调用底层的getFirst方法,说明它取得是第一个元素。

a92c7982829d0fe950b786ca96b3f06b.png

可以看出当队列的实现类为LinkedList时,是可以插入null的,如果把实现类更换为PriorityQueue,会发生什么呢?发现会报空指针异常,原因是优先队列不允许插入null。

4055ff44208f4ac022cd91398cb6d09f.png

以上是使用queue的add,remove和element方法,上述同样的情况下,如果更换成offer,poll和peek方法后会是什么情况,看如下代码测试。

1 packageDataCompose;2

3 importjava.util.LinkedList;4 importjava.util.PriorityQueue;5 importjava.util.Queue;6

7 /**

8 * 测试队列,队列Queue具有先进先出的特点9 */

10 public classQueueTest1 {11

12 public static voidmain(String[] args) {13 //创建一个队列,使用LinkedList来创建对象,并被接口指向

14 Queue queue=new LinkedList();15 //Queue queue=new PriorityQueue();16 //使用会抛出异常的方法,添加元素到尾部,删除头部元素,以及查看头部元素操作17

18 //1 添加元素 offer方法

19 queue.offer("Messi");20 queue.offer("Herry");21 queue.offer(null);22 System.out.println(queue);23

24 //2 删除头部元素 poll方法

25 String str=queue.poll();26 System.out.println(str);27 System.out.println(queue);28

29 //3 查看头部元素 peek方法

30 System.out.println(queue.peek());31

32 }33 }

以上代码正常情况下执行跟第一种情况一模一样的结果,如果集合为空,直接调用poll方法和peek方法,查看执行结果如下,发现输出均为null,说明在集合为空的情况下这两种方法不会抛出异常。

408f12ecf20e45f7dfc1006e4f1eae57.png

同样如果将实现类更换为PriorityQueue,往里面添加null,会是什么结果呢?发现依然抛出异常,主要原因查看offer源码发现,如果实现类不支持null就会抛出异常。

8910a71b9f9d56a8a586087f37f138da.png

另外还有一个deque,是queue的子接口,为双向队列,可以有效的在头部和尾部同时添加或删除元素,其实现类为ArrayDeque和LinkedList类,如果需要一个循环数组队列,选择ArrayDeque,如果需要一个链表队列,使用实现了Queue接口的LinkedList类。

由于Deque实现了Queue接口,因此其可以表现为完全的Queue特征,同时也可以当做栈来使用,具体是Queue还是栈,根据执行的方法来选择,一般来说如果添加元素和删除元素都是在同一端执行(方法后面都为First),就表现为栈的特性,否则就是Queue的特性,以下是Deque接口的方法。

feffe52b7a2377f4fef0b2328c43397d.png

从以上的方法列表中,大概可以总结出以下几个特点:

(1)凡是以add,remove和get开头的方法,都可能在执行的过程中抛出异常,而以offer,poll和peek的方法往往返回null或者其他。

(2)凡是方法后面有Queue接口标志的方法,说明其是继承自接口Queue的方法,有Collection标志的说明是继承自Collection接口的通用方法。

Deque方法参考博文,分类总结如下:

deque和栈的方法对照表

e3ecd3f2af4b9d2a051bfa81706a1929.png

deque和queue的方法对照表

4f0df2e8f3a88f86896f0a594b7f9e8a.png

deque中抛出异常和返回其他值的方法

a54f6be7ddc94cb9372c507cb920b823.png

下面简单的用deque的方法来实现集合操作,从队列两端添加,删除和查看元素,和栈以及queue的相关方法不在这里测试了,未来工作中继续感受。

1 packageDataCompose;2

3 importjava.util.Deque;4 importjava.util.LinkedList;5

6 /**

7 * 测试双向队列,其可以表现为Queue,也可以表现为Stack,这里测试双向列队8 */

9 public classDequeTest {10

11 public static voidmain(String[] args) {12 //使用链表实现

13 Deque deque=new LinkedList<>();14

15 //先在head插入元素

16 deque.offerFirst("Messi");17 deque.offerFirst("Ronald");18 deque.offerFirst("Herry");19 System.out.println(deque);20

21 //在tail插入元素

22 deque.offerLast("clyang");23 System.out.println(deque);24

25 //在head查看元素

26 System.out.println(deque.peekFirst());27

28 //在tail查看元素

29 System.out.println(deque.peekLast());30

31 //在head删除元素

32 deque.pollFirst();33 System.out.println(deque);34

35 //在tail删除元素

36 deque.pollLast();37 System.out.println(deque);38

39 }40 }

控制台输出结果,可以看出deque可以在head和tail两端进行插入、删除和查看操作。

af2030d18740a0ff816888897f09f942.png

数组

数组(Array),是一种有序的元素序列,数组在内存中开辟一段连续的空间,并在连续的空间存放数据,查找数组可以通过数组索引来查找,因此查找速度快,但是增删元素慢。数组创建以后在程序运行期间长度是不变的,如果要增加一个元素,会创建一个新的数组,将新元素存储到索引位置,并将原数组根据索引一一复制到新数组,原来的数组被gc回收,新数组的内存地址赋值给数组变量。

关于数组部分,直接可以从自己写的博客查看具体内容,博客地址:https://www.cnblogs.com/youngchaolin/p/10987960.html,另外参考了大牛博客,进行一些知识面的扩展。

底层利用数组,也可以实现数据结构的基本功能,简单概括一下,就是需要具备增删改查循环遍历的功能,这样才能算实现基本的数据结构,下面参考博客,进行这些功能的封装,实现一个基于数组的简单数据结构。

1 packageDataCompose;2

3 /**

4 * 数组测试,理解最基本数据结构,利用数组封装一个简单的数据结构,实现增删改查和循环遍历5 */

6 public classArrayTest {7 //底层数组,使用Object类型

8 privateObject[] arr;9 //数组占用长度

10 private intlength;11 //数组容量

12 private intmaxSize;13

14 //默认构造方法,仿造ArrayList,默认长度为10

15 publicArrayTest() {16 this.length=0;17 this.maxSize=10;18 arr=newObject[maxSize];19 }20

21 //自定义数组长度

22 public ArrayTest(intmaxSize) {23 this.maxSize =maxSize;24 this.length=0;25 arr=newObject[maxSize];26 }27 //增加元素

28 public booleanadd(Object obj){29 //增加元素暂时不使用底层再创建一个新的数组,进行数组内容复制,参考博客直接添加

30 if(length==maxSize){31 System.out.println("数组容量达到极限,无法自动扩容");32 return false;33 }34 //原数组后面再添加一个元素,否则就是初始值null

35 arr[length++]=obj;36 return true;37 }38 //查找元素,本来先要写删,但是删元素之前需要先查是否存在,因此先写查询方法

39 public intfind(Object obj){40 int n=-1;41 for (int i= 0; i< length; i++) {42 if(obj.equals(arr[i])){43 n=i;44 break;45 }46 }47 returnn;48 }49 //删除元素

50 public booleanremove(Object obj){51

52 if(obj==null){53 System.out.println("不能删除null,请输入正常内容");54 return false;55 }56 int index=find(obj);57 if(index==-1){58 System.out.println("不存在的元素:"+obj);59 return false;60 }61 //数组元素覆盖操作

62 if(index==length-1){63 length--;64 }else{65 for(int i=index;i

73 public boolean modify(intindex,Object obj){74 if(index<0||index>length-1){75 System.out.println("数组下标越界");76 return false;77 }else{78 arr[index]=obj;79 return true;80 }81 }82 //遍历输出内容

83 public voidtoArrayString() {84 System.out.print("[");85 for (int i = 0; i < length; i++) {86 if(i

94 System.out.println();95 }96

97 }

测试类来测试上面写的数据结构。

1 packageDataCompose;2

3 /**

4 * 测试自己底层用数组写的数据结构5 */

6 public classTestArrayTest {7

8 public static voidmain(String[] args) {9 ArrayTest arr=new ArrayTest(5);10 //添加元素

11 arr.add("boy you will have girl");12 arr.add(true);13 arr.add("how many would you like");14 arr.add(1);15

16 //打印数组

17 arr.toArrayString();18

19 //查询为1的元素

20 System.out.println(arr.find(1));21

22 //查询'你好'

23 System.out.println(arr.find("你好"));24

25 //修改下标为3的数组为100

26 arr.modify(3,100);27 arr.toArrayString();28

29 //再添加一个元素

30 arr.add("哈哈哈");31 arr.toArrayString();32

33 //继续添加

34 arr.add("ok?");35

36 //删除最后的元素

37 boolean result=arr.remove("哈哈哈");38 System.out.println(result);39 arr.toArrayString();40

41 }42 }

控制台输出情况,发现可以正常的实现增删改查和循环遍历的功能。

62b89b8ea0c485353b6dde9d3a50576c.png

链表

链表(linked list),是由一系列结点node组成,结点包含两个部分,一个是存储数据的数据域,一个是存储下一个节点地址以及自己地址的地址域,即链表是双向链接的(double linked),多个节点通过地址进行连接,组成了链表,其特点是增删元素快,只要创建或删除一个新的节点,内存地址重新指向规划就行,但是查询元素慢,需要通过连接的节点从头开始依次向后查找。

链表有单向链表和双向链表之分。

单向链表:链表中只有一条'链子',元素存储和取出的顺序可能不一样,不能保证元素的顺序。

双向链表:链表中除了有单向链表一条链子外,还有一条链子用于记录元素的顺序,因此它可以保证元素的顺序。

单向链表的实现

依然参考博主系列文章的链表,自己实现一个自定义的链表,并具有增加头部元素、删除指定元素、修改指定元素、查找元素以及展示链表内容等功能。

1 packageDataCompose;2

3 /**

4 * 单向列表测试,5 */

6 public classSingleLinkTest {7 //定义链表大小

8 private intsize;9 //定义头节点,只需要定义一个头,其他元素都可以通过这个节点头来找到

10 privateNode head;11

12 publicSingleLinkTest() {13 this.size = 0;14 this.head = null;15 }16

17 //在链表头部增加元素

18 publicObject addHead(Object obj) {19 //得到一个新的节点

20 Node newNode = newNode(obj);21 //链表为空,将头元素数据设置为obj

22 if (size == 0) {23 head =newNode;24 } else{25 newNode.next =head;26 head =newNode;27 }28 this.size++;29 returnobj;30 }31

32 //在链表中删除元素

33 public booleandelete(Object obj) {34 //要删除一个元素,需要首先找到这个元素,将这个元素前一个元素next属性指向这个元素的下一个元素

35 if (size == 0) {36 System.out.println("链表为空,无法删除!");37 return false;38 }39 //都是从头部开始查询,有找到需要删除的节点就删除,删除后将这个节点前一个节点next属性指向删除节点的下一个节点40 //需要重新指向的话,需要删除节点数据,也需要删除节点前一个节点的数据,刚开始都使用头部节点数据

41 Node previousNode =head;42 Node currentNode =head;43 //什么时候找到这个元素什么时候停止

44 while (!currentNode.data.equals(obj)) {45 //节点往后遍历,寻找下一个节点数据

46 if (currentNode.next == null) {47 System.out.println("已到链表末尾,无需要删除的元素");48 return false;49 } else{50 //重置当前结点和当前结点前一个结点

51 previousNode =currentNode;52 currentNode =currentNode.next;53 }54 }55

56 //能执行到这里说明有需要删除的元素

57 size--;58 if (currentNode ==head) {59 head =currentNode.next;60 } else{61 previousNode.next =currentNode.next;62 }63 return true;64 }65

66 //修改元素

67 public booleanmodify(Object old, Object newObj) {68 if (size == 0) {69 System.out.println("链表为空,无法修改元素");70 return false;71 }72 Node currentNode =head;73 while (!currentNode.data.equals(old)) {74 if (currentNode.next == null) {75 System.out.println("已到链表末尾,无需要删除的元素");76 return false;77 } else{78 currentNode =currentNode.next;79 }80 }81

82 //能执行到这里说明有相同的元素

83 currentNode.data =newObj;84 return true;85

86 }87

88 //查找元素

89 public booleanfind(Object obj) {90 if (size == 0) {91 System.out.println("链表为空");92 }93 Node currentNode =head;94 while (!currentNode.data.equals(obj)) {95 if (currentNode.next == null) {96 System.out.println("已到链表末尾,无查找的元素");97 return false;98 } else{99 currentNode =currentNode.next;100 }101 }102

103 //能执行到这里说明查找到了元素

104 System.out.println("查找元素存在链表中");105 return true;106 }107

108 //遍历输出元素

109 public voidtoLinkString() {110 if (size > 0) {111 if (size == 1) {112 System.out.println("[" + head.data + "]");113 }114 //结点先从头部开始

115 Node currentNode =head;116 for (int i = 0; i < size; i++) {117 if (i == 0) {118 System.out.print("[" +currentNode.data);119 } else if (i < size - 1) {120 System.out.print("--->" +currentNode.data);121 } else{122 System.out.print("--->"+currentNode.data+"]");123 }124 currentNode =currentNode.next;125 }126 } else{127 System.out.println("[]");128 }129 System.out.println();130 }131

132 }

Node外部类,参考很多博客都是写成内部类,也可以写成外部类。

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 packageDataCompose;2

3 /**

4 * 节点,外部类实现,自定义链表用5 */

6 public classNode {7 //数据部分

8 publicObject data;9 //指向下一个节点,没有修改其中值得情况下默认为null

10 publicNode next;11

12 publicNode(Object data) {13 this.data =data;14 }15

16 }

View Code

测试类代码。

1 packageDataCompose;2

3 /**

4 * 测试自定义单向链表5 */

6 public classTestSingleLinkTest {7

8 public static voidmain(String[] args) {9 //默认容量为0的链表

10 SingleLinkTest Link=newSingleLinkTest();11 //头部添加元素

12 Link.addHead("clyang");13 Link.addHead(8848);14 Link.addHead(true);15 Link.toLinkString();16

17 //查找元素8848

18 boolean r=Link.find(8848);19 System.out.println(r);20

21 //更新元素

22 Link.modify(8848,"success people");23 Link.toLinkString();24

25 //删除元素

26 Link.delete("Messi");27 Link.toLinkString();28

29 }30 }

控制台输出结果,发现可以正常表现为链表的功能。

b6c5a063bf3ebc9d13e2b1defef636bf.png

双向链表的实现

参考博客,以及自己的理解,实现一个双向链表,同时可以保持链表的有序,可以根据索引来查找链表内容。

1 packageDataCompose;2

3 importjava.util.Random;4

5 /**

6 * 双向链表,自定义一个双向列表,具备增删改查和循环遍历的功能7 */

8 public classDoubleLinkTest {9 //属性

10 private intsize;11 privateNode head;12 privateNode tail;13

14

15 //内部类,里面定义一个属性保存数据,再定义两个属性分别指向上一个节点以及下一个节点

16 private classNode {17 //数据部分

18 privateObject data;19 //上一个节点和下一个节点的引用

20 privateNode prev;21 privateNode next;22 //序号部分

23 private intnumber;24

25 //构造方法

26 publicNode(Object data) {27 this.data =data;28 }29

30 public Node(Object data, intnumber) {31 this.data =data;32 this.number =number;33 }34 }35

36 //默认构造方法

37 publicDoubleLinkTest() {38 this.size = 0;39 this.head = null;40 this.tail = null;41 }42

43 //往头增加节点

44 public voidaddHead(Object obj) {45 //往头部增加节点,默认索引号为-1

46 Node myNode = new Node(obj, -1);47 //链表为空

48 if (size == 0) {49 head =myNode;50 tail =myNode;51 head.number = 0;52 tail.number = 0;53 } else{54 head.prev =myNode;55 myNode.next =head;56 //头节点重新赋值

57 head =myNode;58 }59 size++;60 //刷新编号,所有索引号往后面移动一位

61 flushNumber(1);62 //System.out.println("头部增加一个结点成功");

63 }64

65 //往尾部添加节点

66 public voidaddTail(Object obj) {67 Node myNode = newNode(obj);68 //链表为空

69 if (size == 0) {70 tail =myNode;71 head =myNode;72 head.number = 0;73 tail.number = 0;74 } else{75 tail.next =myNode;76 myNode.prev =tail;77 //尾部节点重新赋值

78 tail =myNode;79 }80 size++;81 //尾部序号直接赋值,相比头部序号操作简单很多

82 tail.number = size - 1;83 //System.out.println("尾部增加一个结点成功");

84 }85

86 //往链表内部,除了头部节点之前的任意一个结点插入新节点

87 public voidadd(Object obj) {88 if (size == 0) {89 addHead(obj);90 } else if (size == 1) {91 addTail(obj);92 } else if (size >= 2) {93 //根据链表中的现有长度,获取0~长度-1之间的随机数,将这个随机数作为要插入结点的前一个索引号

94 Random ran = newRandom();95 int insertIndex =ran.nextInt(size);96 //得到要插入位置的前后索引编号

97 int before =insertIndex;98 int after = insertIndex + 1;99 //如果before的索引已经达到链表末尾

100 if (before == size - 1) {101 //尾部添加即可

102 addTail(obj);103 } else{104 //获取插入结点的前后结点

105 Node beforeNode =getNodeByIndexAndStartNode(before, head);106 Node afterNode =getNodeByIndexAndStartNode(after, head);107 Node startNode =afterNode;108 //获取要插入的结点

109 Node currentNode = newNode(obj, after);110 //重新连接前后结点

111 beforeNode.next =currentNode;112 currentNode.prev =beforeNode;113 afterNode.prev =currentNode;114 currentNode.next =afterNode;115 //序号更新,将后面一个结点的所有索引号+1

116 int startIndex =after;117 while (startIndex <= size - 1) {118 int newIndex = startNode.number + 1;119 startNode.number =newIndex;120 startNode =startNode.next;121 startIndex++;122 }123 size++;124 }125 System.out.println("当元素大于或等于2个时,随机插入结点成功");126 }127 }128

129 //通过索引位置,找到对应的节点,另外第二个参数代表从头开始找还是从尾开始找

130 public Node getNodeByIndexAndStartNode(intindex, Node node) {131 if (index < 0 || index > size - 1) {132 System.out.println("索引越界,无效索引");133 return null;134 } else{135 //当前结点

136 Node currentNode =node;137 int count =size;138 while (count > 0) {139 if (currentNode.number !=index) {140 //区分node是从头开始还是从尾开始

141 if(node.equals(head)) {142 currentNode =currentNode.next;143 } else if(node.equals(tail)) {144 currentNode =currentNode.prev;145 }146 } else{147 break;148 }149 count--;150 }151 returncurrentNode;152 }153 }154

155 //查找一个节点,可以使用二分查找,调用上面写的底层查找方法

156 public Node findNodeByIndex(intindex) {157 Node deleteNode=null;158 if (size == 0) {159 System.out.println("链表为空");160 return null;161 } else{162 if (index < size / 2) {163 //从头开始寻找

164 Node node =getNodeByIndexAndStartNode(index, head);165 //System.out.println(node.data);

166 deleteNode=node;167 } else{168 //从尾开始寻找

169 Node node =getNodeByIndexAndStartNode(index, tail);170 //System.out.println(node.data);

171 deleteNode=node;172 }173 }174 System.out.println(deleteNode.data);175 returndeleteNode;176 }177

178 //删除一个节点,根据链表中索引来删除

179 public boolean deleteNodeByIndex(intindex) {180 if (index < 0 || index > size - 1) {181 System.out.println("下标越界,无法删除");182 return false;183 } else{184 //判断是否是首尾节点

185 if (index == 0) {186 head =head.next;187 size--;188 //序号重置

189 flushNumber(-1);190 } else if (index == size - 1) {191 tail =tail.prev;192 size--;193 //依然从0开始,无需重置序号

194 } else{195 //中间位置删除

196 Node beforeNode = findNodeByIndex(index - 1);197 Node afterNode = findNodeByIndex(index + 1);198 Node currentNode =findNodeByIndex(index);199 //当前位置节点赋值为null,等待gc自动回收

200 currentNode = null;201 //前后结点重新连接

202 beforeNode.next =afterNode;203 afterNode.prev =beforeNode;204 //重新设置后一个结点后面的索引号

205 Node startNode=afterNode;206 int startIndex=startNode.number;207 while(startIndex<=size-1){208 int newIndex=startNode.number-1;209 startNode.number=newIndex;210 startNode=startNode.next;211 startIndex++;212 }213 size--;214 }215 return true;216 }217 }218

219 //修改一个节点内容

220 public boolean modify(intindex, Object obj) {221 if (index < 0 || index > size - 1) {222 System.out.println("索引越界,无效索引");223 return false;224 } else{225 Node currentNode =findNodeByIndex(index);226 currentNode.data =obj;227 System.out.println("修改成功");228 return true;229 }230 }231

232 //遍历节点

233 public voidtoDoubleLinkArray() {234 if (size > 0) {235 if (size == 1) {236 System.out.println("[" + "(" + head.data + ":" + head.number + ")" + "]");237 return;238 }239 //依然从头部开始遍历

240 Node currentNode =head;241 int count = size;//需要遍历的次数

242 while (count > 0) {243 if(currentNode.equals(head)) {244 System.out.print("[" + "(" + currentNode.data + ":" + currentNode.number + ")");245 } else if (currentNode.next == null) {246 System.out.print("--->" + "(" + currentNode.data + ":" + currentNode.number + ")" + "]");247 } else{248 System.out.print("--->" + "(" + currentNode.data + ":" + currentNode.number + ")");249 }250 //每输出一个往后移动一个节点

251 currentNode =currentNode.next;252 count--;253 }254 //换行

255 System.out.println();256 } else{257 System.out.println("[]");258 }259 }260

261 //更新所有节点顺序编号,往前或者后移动一位

262 public void flushNumber(intnumber) {263 if (size > 1) {264 int count =size;265 Node currentNode =head;266 while (count > 0) {267 if (number != 1&&number != -1) {268 System.out.println("移动数字非法");269 return;270 } else{271 if (number == 1) {272 int newNumber = currentNode.number + 1;273 currentNode.number =newNumber;274 } else if(number==-1){275 int newNumber = currentNode.number - 1;276 currentNode.number =newNumber;277 }278 currentNode =currentNode.next;279 count--;280 }281 }282 System.out.println("链表序号刷新完成");283 } else{284 System.out.println("链表为空,或者无需刷新节点编号");285 }286 }287

288 }

测试代码

1 packageDataCompose;2

3 public classTestDoubleLinkTest {4

5 public static voidmain(String[] args) {6 //测试双向链表

7 DoubleLinkTest Link=newDoubleLinkTest();8 //添加

9 System.out.println("----------开始增加操作----------");10 Link.addHead("Messi");11 Link.toDoubleLinkArray();12

13 Link.addHead("clyang");14 Link.toDoubleLinkArray();15

16 Link.addHead("Ronald");17 Link.toDoubleLinkArray();18

19 Link.addTail("KaKa");20 Link.toDoubleLinkArray();21

22 System.out.println("----------开始修改操作----------");23 //修改位置1的元素

24 Link.modify(1,"Kane");25 Link.toDoubleLinkArray();26

27 System.out.println("----------开始随机插入操作----------");28 //随机插入一个结点

29 Link.add("random");30 Link.toDoubleLinkArray();31

32 //再次随机插入一个结点

33 Link.add("Jodan");34 Link.toDoubleLinkArray();35

36 System.out.println("----------开始查找操作----------");37 //查找位置为2的节点

38 Link.findNodeByIndex(2);39

40 System.out.println("----------开始删除操作----------");41 //删除节点2的位置

42 Link.deleteNodeByIndex(2);43 Link.toDoubleLinkArray();44 }45

46 }

控制台输出情况,可以正常的实现增删改查循环遍历的功能,并且插入删除后依然可以保证节点的顺序。

61e06beb61c5dfcc88e0b35e36b26d10.png

红黑树

树型数据结构

树形结构名词介绍,比较形象,类似于现实中的树,只是计算机中的树其根部在上面,叶子在下面。

结点:树中的一个元素

结点的度:结点拥有的子树的个数,二叉树的话,度不能大于2

高度:叶子节点的高度为1,逐渐往上越来越高,根节点高度是最高的

叶子:高度为0的结点,也是终端结点

层:以根开始是第一层,往下面开始逐一增加

父结点:若一个结点包含若干个子结点,则这个结点就是子结点的父结点

子结点:子结点就是父节点的下一个节点

结点的层次:以根节点开始,根节点为第一层,根的子结点为第二层,逐一类推

兄弟结点:拥有共同父结点的结点称为兄弟结点

平衡因子:该结点左子树的高度-该结点右子树的高度,即平衡因子

二叉树(binary tree)

是每个结点不超过2个子结点的有序树(tree),每个结点上最多只能有两个子结点,顶上的结点叫做根结点,两边的分支分别叫做左子树和右子树。

平衡树

也是基于二叉树,平衡因子的绝对值不能超过1,即左右子树高度差不能达到2。当往父结点上添加一个子结点后破坏了平衡条件,就会进行平衡旋转,有LL、LR、RR和RL四种类型的平衡旋转,为了想看动画演示旋转,可以登录后面参考的网址(https://www.cs.usfca.edu/~galles/visualization/AVLtree.html)即可。

(1)LL型平衡旋转

如果往如图所示的结点6下面再添加一个子结点4,可以分析一下各个结点的平衡因子,叶子结点4的平衡因此为0,父结点6的平衡因子为1,根结点8的平衡因子为2,破坏了二叉树的平衡条件,因此需要右旋,即6上升到根结点,8移动到根结点的右边,作为子结点。

97897063928ba192ba9cf753e9ac36d8.png 旋转后

f375d3c4b45a1d91ca0403ce40dfc573.png

(2)LR型平衡旋转

同样在结点6的下面增加一个子结点7,添加后在6子结点的右边,其平衡依然被破坏,因此也需要旋转,首先7结点的位置会移动到6的位置,6的位置移动到7的左子结点,这个过程为左旋,变成6-7-8的LL型,然后再右旋转,将7上升到根节点,8变成根结点的右结点。

83a0d5df7c5cd33209f32b78f8309016.png旋转后

abd9ec87f189cfdb942f9df890db7396.png

(3)RR型平衡旋转

在父节点10的下面添加一个子结点12,也破坏了平衡性,因此需要左旋,即10上升到根结点的位置,8变成根节点的左子结点。

c300a507e70940ee673b057a7b4bd75f.png左旋转 

b1ace7ac3669e733fa671c9100990d41.png

(4)RL型平衡旋转

在父节点10下面添加子结点9,发现比10小因此为左子结点,与上面情况类似,添加子结点后依然破坏了平衡性,因此需要旋转,首先需要右旋转将9上升到父节点10的位置,将10变成9的右子结点,即变成10-9-8的RR型,这样还需要进行一次左旋转,将9上升到根结点的位置,原根结点8变成9的左子结点。

c976db1ff5eb43621bd40210e16b0f02.png旋转后

e144544bd9dff9fc5f1d53a4d0ccdae7.png

旋转实战演练,先在左子树添加元素,先调整成LL型,然后右旋

往下面平衡树下添加子结点4,可以分析一下,4比20小,往左边子树走,然后比5小,依然往左边子树走,最后到了字结点2,发现比2大因此挂在了2的右子结点,这样显然是破坏了二叉树的平衡的。因此随后需要对2-4-5三个结点进行LR平衡型旋转,首先2左旋,将4移动到2结点的位置,2变成4结点的左子结点,这样就变成了2-4-5的LL型,然后右旋,将4上升到父结点5的位置,而5变成4的右子结点。

6f8588d05cb3bfdf1776168a00327e34.png添加4后

ba1bdb3d4b8801619842080195feef69.png先左旋后右旋

fb63b5117385053c41e4763bf595a979.png

如果继续在上面二叉树的基础上在添加字结点1,显然会破坏平衡,因此4需要上升为根节点,将20进行右旋,20右旋会跟4的右结点5发生碰撞,这种情况就将5挂到20的左边,达到平衡。

47b32b45f62da6cc308d7a9549472c6b.png右旋转

2980ef58028b09343f8010320daff27a.png

如果要在如图所示的树上增加结点6,需要先左旋变成LL型,将7移动到父节点5的位置,5移动到7的左节点,这个时候5和6会发生碰撞,将6挂到5结点的右边,变成LL型,然后将7上升到根结点,然后进行右旋,将20和30都挂到7的右边。

d03f2c456a61dd7787b593f935d53bbf.png先左旋后右旋

236ed36c1df70fde84f7d7f1f89c6302.png

如果在如图所示的树上添加8,依然先需要进行左旋变成LL型,将7上升到父结点5的位置,5和2变成7的左子树,8变成7的的右结点,然后进行右旋,将7上升到根结点的位置,这样20和8都为7的右结点会发生碰撞,将8挂到20的下面。

72b4b257aae9ad89edff50598653a130.png先左旋后右旋

8c8222030b0730d7882b7c5a35b090d9.png

旋转实战演练,在右子树添加元素,先调整成RR型,然后左旋

如果在二叉树中添加结点60,破坏了二叉树的平衡,由于右子树已经是RR型,因此需要左旋就行,30左旋变成50的子结点,50上升到原先30的位置。

9188903b5efb0531b0d22aabcb58acb2.png左旋

084c9700cf00d40c76b02def6e37b0c5.png

继续在上面的二叉树上添加结点70,发现右边也已经是RR型,无需调整,只要将50上升到根结点,将20结点进行左旋即可,左旋后20和30会发生碰撞,由于30比20大因此及会挂在20的右边。

af3cf60e902ff3eddfc235b7e25a640a.png已经是RR型,直接左旋

3d675c1abb66e8211b08938b1d1f0157.png

如果不在50结点上添加子结点,在25结点上添加一个右子节点,会稍微复杂一点,首先需要将右子树变成RR型,需要先右旋一次,将25上升到30父节点的位置,30变成25的右边子结点,这样30和28会发生碰撞,由于28比30小因此变成其左子结点,这样右边变成RR型,然后将20根结点进行一次左旋,就达到平衡。

e84905c61064e9e639438300650a2008.png先右旋变成RR型,然后左旋

18feb3103c16ec63486da2d0d3a6476b.png

如果不在50结点上添加子结点,在25结点上添加一个左子节点,依然需要先将右子树进行右旋,这样25上升到30的位置,25父节点下面左子结点为23,右子树为30和50,这样变成RR型,然后将20左旋,这样20和23会发生碰撞,由于23比20大因此会挂到20的右边,达到平衡。

3598ac53f4068d85820787827e9c9bab.png先右旋变成RR型,然后左旋

7906dd8c5cab708b604cdc3527fa4cc2.png

不平衡树

基于二叉树,左子树和右子树数量不相等,在极端情况下可能导致查询变成类似查询链表的情况。

排序树/查找树

排序树是基于二叉树,左子树数值小,右子树数值大,查找数字会很快。

红黑树

红黑树(Red Black Tree)是一种自平衡二叉查找树,在1972年由Rudolf Bayer发明,称为平衡二叉B树,后面被修改为红黑树,红黑树是一种特殊的二叉查找树,其每个节点不是红就是黑。其查找叶子节点的最大次数和最小次数不能超过2倍,跟平衡二叉树不太一样的是,它不需要满足平衡因子绝对值<=1。

红黑树还有以下约束:

1 节点可以是红色或者黑色的

2 根节点是黑色的

3 叶子节点(NIL节点,空节点)是黑色的

4 每个红色节点的子节点都是黑色,即每个叶子到根的路径上不会存在两个连续的红色节点

5 任何节点,到其下面每一个叶子节点的路径上,黑色节点数是相同的

黑红树的主要用途就是使用在HashMap,TreeMap中。

红黑树左旋右旋

红黑树的左旋和右旋,可以参考上面平衡二叉树的左旋和右旋规律,基本一样。

红黑树的插入操作

红黑树如果做插入结点操作,会改变原始红黑树的结构,一般插入的是红色结点,并且有相应的恢复策略,参考了哔哩哔哩视频(https://www.bilibili.com/video/av53772633/?p=3&t=275),大致上可以按照以下的规则来,但是红黑树还有自己的约束,因此个人感觉可以将下面的规则做为参考,但是不一定实际中完全适用,如约束中的第5条,就会导致下面部分规则不完全适用。

(1)插入的是根结点

原始树是空树,因此根节点为红色,违反上面约束条件第二条

策略:将结点涂成黑色

(2)插入结点父结点是黑色

策略:红黑树没有破坏,不需要修复

(3)插入结点后,当前结点的父结点是红色,并且叔叔结点也是红色。

策略:当前结点'4'的父结点'5'和叔叔结点'8'全部变成黑色,祖父结点'7'变成红色。

e17e42c38fcf1e33178e56ae4ea865e3.png变化后

4ef967e30377820c48e4095084125d03.png

(4)当前结点的父结点是红色,叔叔结点是黑色,并且当前结点是父结点的右结点。

策略:当前结点'7'的父结点'2'为支点左旋,当前结点'7'上移动。如果当前结点是父结点的左结点,后面再补充。如图'2'和'5'的结点左旋会发生碰撞,由于'5'比'2'大因此将'5'结点对应的子树挂在结点'2'的右子树。

4ef967e30377820c48e4095084125d03.png左旋

895c793bc90e8f980a36b392d58f2920.png

(5)当前结点的父结点是红色,叔叔结点是黑色,当前结点是父结点的左结点

策略:当前结点'2'的父结点'7'变成黑色,祖父结点'11'变成红色,以祖父结点'11'为支点进行右旋。右旋时'11'和'8'会发生碰撞,由于'8'比'11'小因此'8'结点对应子树挂在'11'结点的左边作为左子树。

895c793bc90e8f980a36b392d58f2920.png变色以及右旋

bbf4a80c7b9270a5ac4735a5a0e93c45.png

红黑树添加结点实战演练

如图在红黑树的结点上添加20,刚开始作为50的左子结点,这样不符合红黑树的规则,并且这样的情况满足上面说的情况5,因此50结点会变成黑色,根结点右旋。根据动画可以看出来,先完成右旋,再完成变色。

aeb50fb7b2f7926b6806a134c0e03b67.png旋转

e54b25ccf04b3db1361fa6fd5c492a00.png变色

07fad954e934dd4fb012f36d48454dc7.png

继续添加结点200,首先会作为100的右结点添加,这种符合上面说的情况3,因此父结点和叔叔结点都变成黑色,祖父结点50变成红色,然后根节点不能为红色,因此继续变色,最后根节点变成黑色。需要注意的是红色节点的子结点必须为黑色节点,但是没有规定黑色节点的子结点必须为红色,说明黑色节点下面子结点什么颜色都可以。

4e1fada63e6a1c5ad47ee0d781eda4b9.png变色

2d8924b5a8bbb735cd241e297b5d85ea.png继续变色

ca4530a2e779954363ba9280b5bf39d9.png

继续添加结点300,首先会作为子结点添加到200的右子结点,这种符合上面说的情况5,因此首先以插入结点的祖父结点100为支点发生左旋,然后变色,父结点200变成黑色,原祖父结点变成红色。

1d021aedb1d8a38567f5aac791d87cdb.png左旋

59fb211f1ef260b5a4cc55263ceac719.png变色

fc12c28483424f2d27437113301f14e9.png

继续添加结点150,首先会作为子结点添加到100的右子结点,这种符合上面的情况3,因此父结点和叔叔结点变成黑色,祖父结点200变成红色,变完色后发现符合黑红树规则,无需再旋转或变色。

0d02d36d8206a7536f5bb27aaac9484d.png变色

9a6a09e392fda54d4be72ac3c5bbf7a3.png

继续添加元素160,会先作为右结点挂在150下面,然后这种情况符合上面第五种,因此先按照祖父结点100为支点左旋,然后父结点变成黑色,原祖父结点变成红色,完后发现符合黑红树规则,无需再选择或变色。

a7eb8530f2fa0d8f65fb61e9068ec097.png左旋

90281cac22ee14408c6b846e2c28cb08.png变色

3b244cf73c6bcbdb46e6b71ff2376730.png

添加320到子结点,会首先挂在350下面,然后这种符合第四种情况,父结点是红色,叔叔结点(null)为黑色,由于当前结点在父结点的左边,因此先以父结点350为支点右旋,右旋后变成上面的第五种情况,因此先以祖父结点300左旋,然后父结点320变色为黑色,原祖父结点300变色为红色。

13ab897c8bffb4f65db617446bb4cdbc.png右旋

905170b8e2103c3755302976e9785ff7.png左旋变色

305e563d89b07992c1e829977b7b049a.png

最后添加180到结点中,这个添加会将上面说的第三四五种情况都包含,首先添加到160的右子结点,这种符合第三种情况,因此父结点和叔叔结点都变色为黑色,祖父结点150变成红色。

419a97c9195aadffbcf9066f73a82294.png变色

e7fd0e87b402441bb7ef736435daefc5.png右旋

变成红色后,这种为第四种情况,即150结点的父结点是红色,叔叔结点是黑色,因此本例中需要右旋,由于右旋后200和160会碰撞,因此160结点的子树将作为200结点的左子树。

d928a5a78892648b43585571d80def72.png左旋

然后变成了第五种情况,只是在右边,因此需要以200结点的祖父结点50为支点左旋,由于左旋后,50和100会发生碰撞,因此100将挂在50结点的右边。并且父结点150变成黑色,祖父结点50会变成红色。

17bffe5183e2812c1995a8e72d9bd199.png变色

6362f69f7b9a855290130eef9faa701f.png

关于红黑树的删除结点后面再添加,后续完善。

总结

以上是对数据结构基础的复习和理解,包括栈、队列、数组、链表和红黑树,还有很多理解不到位的地方,后续工作和学习中再补充添加,感谢引用的各位博文博主。

参考博文:

(1)《Java核心技术卷》第8版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值