數據結構部分,復習棧,隊列,數組,鏈表和紅黑樹,參考博客和資料學習后記錄到這里方便以后查看,感謝被引用的博主。
棧
棧(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 }
測試類代碼。
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 平衡二叉樹旋轉