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 平衡二叉樹旋轉


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM