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 package dataStructure;
 2 /**
 3  * 自定义栈,使用数组来实现
 4  */
 5 public class MyStack {
 6     
 7     private int size;//数组大小
 8     private String[] arr;//数组
 9     private int top=-1;//默认栈顶位置
10     
11     //构造方法
12     public MyStack(int size) {
13         this.size = size;
14         arr=new String[size];
15     }
16     
17     //压栈
18     public void push(String value){
19         //top范围0到size-1
20         if(top<=size-2){
21             arr[++top]=value;
22         }
23     }
24     
25     //出栈
26     public String pop(){
27         //原栈顶元素设置为null,等待gc自动回收
28         return arr[top--];
29     }
30     
31     //查看栈顶
32     public String peek(){
33         if(top>-1){
34             return arr[top];
35         }else{
36             return null;
37         }
38     }
39     
40     //检查栈是否为空
41     public boolean Empty(){
42         return top<0;
43     }
44     
45     //检查栈是否满
46     public boolean Full(){
47         return top==size-1;
48     }
49     
50     //检查栈中元素数量
51     public String size(){
52         int count=top+1;
53         return "栈中元素:"+count+" | 栈容量"+size;
54     }    
55             
56 }

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

 1 package dataStructure;
 2 
 3 public class TestMyStack {
 4 
 5     public static void main(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 }

控制台输出情况。

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

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

 1 package dataStructure;
 2 
 3 import java.util.Arrays;
 4 import java.util.EmptyStackException;
 5 
 6 /**
 7  * 自定义栈,使用数组来实现,可以实现数组自动扩容,以及存储不同的数据类型
 8  */
 9 public class MyArrayStack {
10     //定义属性
11     private int size;//容量
12     private Object[] arr;//对象数组
13     private int top;//栈顶位置
14     
15     //默认构造方法
16     public MyArrayStack() {
17         this.size=10;
18         this.arr=new Object[10];
19         this.top=-1;
20     }
21 
22     //自定义数组容量的构造方法
23     public MyArrayStack(int size) {
24         if(size<0){
25             throw new IllegalArgumentException("栈容量不能小于0"+size);
26         }
27         this.size = size;
28         this.arr=new Object[size];
29         this.top=-1;
30     }
31     
32     //压栈
33     public Object push(Object value){
34         //压栈之前欠判断数组容量是否足够,不够就扩容
35         getNewCapacity(top+1);
36         arr[++top]=value;
37         return value;        
38     }
39     
40     //出栈
41     public Object pop(){
42         if(top==-1){
43             throw new EmptyStackException();
44         }
45         Object obj=arr[top];
46         //删除原来栈顶的位置,默认设置为null,等待gc自动回收
47         arr[top--]=null;
48         return obj;
49     }
50     
51     //查找栈顶的元素
52     public Object peek(){
53         return arr[top];
54     }
55     
56     //判断栈是否为空
57     public boolean Empty(){
58         return top==-1;
59     }
60     
61     //检查栈中元素
62     public String size(){
63         int count=top+1;
64         return "栈中元素:"+count+" | 栈容量"+size;
65     }
66     
67     //返回栈顶到数组末端内容
68     public void printWaitPosition(){
69         if(top<arr.length-1){
70             for(int i=top+1;i<arr.length;i++){
71                 System.out.println("空闲位置数值为:"+arr[i]);
72             }            
73         }else{
74             System.out.println("没有空闲位置");
75         }
76     }
77     
78     
79     //写一个方法判断数组是否需要自动扩容
80     public boolean getNewCapacity(int index){
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 package dataStructure;
 2 
 3 public class TestMyArrayStack {
 4 
 5     public static void main(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 }

控制台输出情况。

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

队列

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

简单的整理一下如下。

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

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

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

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

使用会抛出异常的方法

 1 package DataCompose;
 2 
 3 import java.util.LinkedList;
 4 import java.util.PriorityQueue;
 5 import java.util.Queue;
 6 
 7 /**
 8  * 测试队列,队列Queue具有先进先出的特点
 9  */
10 public class QueueTest {
11 
12     public static void main(String[] args) {
13         //创建一个队列,使用LinkedList来创建对象,并被接口指向
14         Queue<String> queue=new LinkedList<String>();
15         //Queue<String> queue=new PriorityQueue<String>();
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方法也是先查询得到头部元素,因此遵循先进先出原则。

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

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

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

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

 1 package DataCompose;
 2 
 3 import java.util.LinkedList;
 4 import java.util.PriorityQueue;
 5 import java.util.Queue;
 6 
 7 /**
 8  * 测试队列,队列Queue具有先进先出的特点
 9  */
10 public class QueueTest1 {
11 
12     public static void main(String[] args) {
13         //创建一个队列,使用LinkedList来创建对象,并被接口指向
14         Queue<String> queue=new LinkedList<String>();
15         //Queue<String> queue=new PriorityQueue<String>();
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,说明在集合为空的情况下这两种方法不会抛出异常。

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

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

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

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

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

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

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

deque和栈的方法对照表

deque和queue的方法对照表

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

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

 1 package DataCompose;
 2 
 3 import java.util.Deque;
 4 import java.util.LinkedList;
 5 
 6 /**
 7  * 测试双向队列,其可以表现为Queue,也可以表现为Stack,这里测试双向列队
 8  */
 9 public class DequeTest {
10 
11     public static void main(String[] args) {
12         //使用链表实现
13         Deque<String> 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两端进行插入、删除和查看操作。

数组

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

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

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

 1 package DataCompose;
 2 
 3 /**
 4  * 数组测试,理解最基本数据结构,利用数组封装一个简单的数据结构,实现增删改查和循环遍历
 5  */
 6 public class ArrayTest {
 7     //底层数组,使用Object类型
 8     private Object[] arr;
 9     //数组占用长度
10     private int length;
11     //数组容量
12     private int maxSize;
13 
14     //默认构造方法,仿造ArrayList,默认长度为10
15     public ArrayTest() {
16         this.length=0;
17         this.maxSize=10;
18         arr=new Object[maxSize];
19     }
20 
21     //自定义数组长度
22     public ArrayTest(int maxSize) {
23         this.maxSize = maxSize;
24         this.length=0;
25         arr=new Object[maxSize];
26     }
27     //增加元素
28     public boolean add(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 int find(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         return n;
48     }
49     //删除元素
50     public boolean remove(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<length-1;i++){
66                 arr[i]=arr[i+1];
67             }
68             length--;
69         }
70         return true;
71     }
72     //修改元素,直接修改数组索引上的元素
73     public boolean modify(int index,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 void toArrayString() {
84         System.out.print("[");
85         for (int i = 0; i < length; i++) {
86             if(i<length-1){
87                 System.out.print(arr[i] + ",");
88             }else{
89                 System.out.print(arr[i]);
90             }
91         }
92         System.out.print("]");
93         //换行
94         System.out.println();
95     }
96 
97 }

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

 1 package DataCompose;
 2 
 3 /**
 4  * 测试自己底层用数组写的数据结构
 5  */
 6 public class TestArrayTest {
 7 
 8     public static void main(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 }

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

链表

链表(linked list),是由一系列结点node组成,结点包含两个部分,一个是存储数据的数据域,一个是存储下一个节点地址以及自己地址的地址域,即链表是双向链接的(double linked),多个节点通过地址进行连接,组成了链表,其特点是增删元素快,只要创建或删除一个新的节点,内存地址重新指向规划就行,但是查询元素慢,需要通过连接的节点从头开始依次向后查找。
链表有单向链表和双向链表之分。
单向链表:链表中只有一条'链子',元素存储和取出的顺序可能不一样,不能保证元素的顺序。
双向链表:链表中除了有单向链表一条链子外,还有一条链子用于记录元素的顺序,因此它可以保证元素的顺序。

单向链表的实现

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

  1 package DataCompose;
  2 
  3 /**
  4  * 单向列表测试,
  5  */
  6 public class SingleLinkTest {
  7     //定义链表大小
  8     private int size;
  9     //定义头节点,只需要定义一个头,其他元素都可以通过这个节点头来找到
 10     private Node head;
 11 
 12     public SingleLinkTest() {
 13         this.size = 0;
 14         this.head = null;
 15     }
 16 
 17     //在链表头部增加元素
 18     public Object addHead(Object obj) {
 19         //得到一个新的节点
 20         Node newNode = new Node(obj);
 21         //链表为空,将头元素数据设置为obj
 22         if (size == 0) {
 23             head = newNode;
 24         } else {
 25             newNode.next = head;
 26             head = newNode;
 27         }
 28         this.size++;
 29         return obj;
 30     }
 31 
 32     //在链表中删除元素
 33     public boolean delete(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 boolean modify(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 boolean find(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 void toLinkString() {
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外部类,参考很多博客都是写成内部类,也可以写成外部类。

 1 package DataCompose;
 2 
 3 /**
 4  * 节点,外部类实现,自定义链表用
 5  */
 6 public class Node {
 7     //数据部分
 8     public Object data;
 9     //指向下一个节点,没有修改其中值得情况下默认为null
10     public Node next;
11 
12     public Node(Object data) {
13         this.data = data;
14     }
15 
16 }
View Code

测试类代码。

 1 package DataCompose;
 2 
 3 /**
 4  * 测试自定义单向链表
 5  */
 6 public class TestSingleLinkTest {
 7 
 8     public static void main(String[] args) {
 9         //默认容量为0的链表
10         SingleLinkTest Link=new SingleLinkTest();
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 }

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

双向链表的实现

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

  1 package DataCompose;
  2 
  3 import java.util.Random;
  4 
  5 /**
  6  * 双向链表,自定义一个双向列表,具备增删改查和循环遍历的功能
  7  */
  8 public class DoubleLinkTest {
  9     //属性
 10     private int size;
 11     private Node head;
 12     private Node tail;
 13 
 14 
 15     //内部类,里面定义一个属性保存数据,再定义两个属性分别指向上一个节点以及下一个节点
 16     private class Node {
 17         //数据部分
 18         private Object data;
 19         //上一个节点和下一个节点的引用
 20         private Node prev;
 21         private Node next;
 22         //序号部分
 23         private int number;
 24 
 25         //构造方法
 26         public Node(Object data) {
 27             this.data = data;
 28         }
 29 
 30         public Node(Object data, int number) {
 31             this.data = data;
 32             this.number = number;
 33         }
 34     }
 35 
 36     //默认构造方法
 37     public DoubleLinkTest() {
 38         this.size = 0;
 39         this.head = null;
 40         this.tail = null;
 41     }
 42 
 43     //往头增加节点
 44     public void addHead(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 void addTail(Object obj) {
 67         Node myNode = new Node(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 void add(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 = new Random();
 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 = new Node(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(int index, 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             return currentNode;
152         }
153     }
154 
155     //查找一个节点,可以使用二分查找,调用上面写的底层查找方法
156     public Node findNodeByIndex(int index) {
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         return deleteNode;
176     }
177 
178     //删除一个节点,根据链表中索引来删除
179     public boolean deleteNodeByIndex(int index) {
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(int index, 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 void toDoubleLinkArray() {
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(int number) {
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 package DataCompose;
 2 
 3 public class TestDoubleLinkTest {
 4 
 5     public static void main(String[] args) {
 6         //测试双向链表
 7         DoubleLinkTest Link=new DoubleLinkTest();
 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 }

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

红黑树

树型数据结构

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

结点:树中的一个元素

结点的度:结点拥有的子树的个数,二叉树的话,度不能大于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移动到根结点的右边,作为子结点。

 旋转后

(2)LR型平衡旋转

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

旋转后

(3)RR型平衡旋转

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

左旋转 

(4)RL型平衡旋转

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

旋转后

旋转实战演练,先在左子树添加元素,先调整成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的右子结点。

添加4后先左旋后右旋

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

右旋转

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

先左旋后右旋

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

先左旋后右旋

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

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

左旋

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

已经是RR型,直接左旋

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

先右旋变成RR型,然后左旋

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

先右旋变成RR型,然后左旋

不平衡树

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

排序树/查找树

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

红黑树

红黑树(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'变成红色。

变化后

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

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

左旋

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

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

变色以及右旋

红黑树添加结点实战演练 

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

旋转变色

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

变色继续变色

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

左旋变色

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

变色

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

左旋变色

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

右旋左旋变色

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

变色右旋

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

左旋

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

变色

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

总结

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

 

参考博文:

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

(2)https://www.cnblogs.com/ysocean/p/7911910.html 栈

(3)https://www.cnblogs.com/youngchaolin/p/10463887.html 二进制

(4)https://segmentfault.com/a/1190000016524796 队列

(5)https://www.cnblogs.com/ysocean/p/7894448.html 数组

(6)https://www.cnblogs.com/ysocean/p/7928988.html 链表

(7)https://www.cnblogs.com/shamo89/p/6774080.html 队列

(8)https://www.cnblogs.com/ChenD/p/7814906.html 双向链表

(9)https://segmentfault.com/a/1190000014037447?utm_source=tag-newest  红黑树

(10)https://www.cs.usfca.edu/~galles/visualization/AVLtree.html 生成平衡二叉树动画

(11)https://www.cs.usfca.edu/~galles/visualization/RedBlack.html 生成红黑树动画

(12)https://www.cnblogs.com/lezhifang/p/6632355.html 平衡二叉树旋转

posted @ 2019-06-21 08:54  斐波那切  阅读(1514)  评论(2编辑  收藏  举报