java資料結構和演算法10(堆)
這篇我們說說堆這種資料結構,其實到這裡就暫時把java的資料結構告一段落,感覺說的也差不多了,各種常見的資料結構都說到了,其實還有一種資料結構是“圖”,然而暫時對圖沒啥興趣,等有興趣的再說;還有排序演算法,emmm....有時間再看看吧!
其實從寫資料結構開始到現在讓我最大的感觸就是:新手剛開始還是不要看資料結構為好,太無聊太枯燥了,很容易讓人放棄;可以等用的各種框架用得差不多了之後,再回頭靜下心來搞搞資料結構還是挺有趣的;廢話不多說,開始今天的內容:
1.二叉樹分類
樹分為二叉樹和多叉樹,其實吧有個很有趣的現象,如果樹為一叉也就是一個連結串列(當然沒有一叉樹這個說法啊,這裡是為了好理解);如果為二叉就是二叉樹(包括二叉搜尋樹,紅黑樹等);如果為二叉以上就是為多叉樹(包括2-3樹,2-3-4樹,B樹等)
我們再來簡單看看二叉樹,通過前面的學習我們知道了二叉樹是什麼鬼,一句話概括就是:除了葉節點外其他的節點最多隻有兩個子節點;這裡我們還可以繼續對二叉樹進行分類,可以簡單的分為完美二叉樹,完全二叉樹,滿二叉樹,我們就簡單說說這三種分類;
完美二叉樹:跟名字差不多,很完美;除了葉節點之外,任意節點都有兩個子節點,整棵樹可以構成一個大三角形,下圖所示:
完全二叉樹:沒有上面的那麼完美,只需要從根節點到倒數第二層滿足完美二叉樹,而最後一層不需要都填滿,填滿一部分即可,但是最後一層有個要求:從最左邊的節點到最右邊的節點中間不能有空位置,例如下圖:
滿二叉樹:比完全二叉樹要求更低,只需要保證除葉節點外,其他節點都有兩個子節點,下面兩個圖都是滿二叉樹:
2.堆的簡介
首先我們要明白,此堆非彼堆,我們這裡的堆是一種資料結構而不是jvm中的堆記憶體空間;深入一點來說,堆是一種完全二叉樹,通常用陣列實現,而且堆中任意一個節點資料都要大於兩個子節點的資料,下圖所示:
注意,只要子節點兩個子節點的資料都比父節點小即可,兩個子節點誰大誰小無所謂;
由於堆是由陣列實現的,那麼陣列到底是怎麼儲存堆中的資料的呢?看下圖,其實就是先將第一層資料放入陣列,然後將第二層資料從左到右放入陣列,然後第三層資料從左到右放入陣列....
這裡補充一個小知識點,後面寫java程式碼實現的時候會用到:在這裡我們可以知道堆中的每一個節點都對應於陣列中的一個位置,,所以可以根據任意一個節點的位置得出來父節點的位置和兩個子節點的位置;舉個例子,50這個節點的陣列下標是5,那麼父節點下標為(5-1)/2向下取整等於2,左子節點下標為2*5+1 = 11;右子節點下標為2*5+2 = 12,我們可以簡單歸納一下:
假如一個節點對應的陣列下標為a,那麼父節點為:(a-1)/2向下取整,左子節點:2a+1,右子節點:2a+2
3.堆的操作
從上面這個圖可以看出來堆中的資料放在陣列中是沒有強制性的順序的(這裡叫做弱序),只能知道陣列第一個數最大,最小的數不知道在哪裡,而且父節點肯定要比子節點大,除此之外我們就什麼也不知道了;
對於資料結構的操作而言無非就是增刪改查,我們可以知道在堆中是沒有辦法進行查詢的,因為左右節點沒有規定必須哪個大那個小,所以查詢的時候不知道應該往哪個子節點去比較,一般而言修改操作是建立在查詢的基礎上的,所以堆也不支援修改,還有還不支援遍歷操作,這樣算下來,堆就支援增加和刪除操作了,那麼下面我們就看看這兩個操作;
3.1新增節點
新增節點分為兩種情況,第一種,新增的節點資料非常小,直接放在堆的最後一個位置即可(換句話說直接新增到陣列有效長度的後面一個空位置即可),這種情況很容易就不多介紹了;第二種,新增節點的資料稍微比較大,比如下面這樣的:
此時堆的結構就被破壞了,我們需要向上調整,那麼應該怎麼調整呢?很容易,直接和父節點交換位置即可,直到滿足堆的這個條件:任意一個父節點都要大於子節點資料
3.2.刪除節點
這裡的刪除指的是刪除堆中最大的節點,換句話說就是每次刪除都是刪除根節點(對應於陣列的第一個元素)
但是刪除過後堆的結構就會被破壞,於是要進行調整來平衡堆的結構,看看下圖:
我們的做法就是將堆中最後一個節點放在根節點那裡(對應於陣列就是將陣列有效長度的最後元素放在第一個位置那裡),然後判斷新的根節點是不是比兩個子節點中最大的那個還要大?是,不需要調整;否,則將此時新的根節點與比較大的子節點交換位置,然後無限重複這個交換步驟,直到該節點的資料大於兩個子節點即可;
3.3.換位
上面說的換位是什麼意思呢?我們知道在java中要交換兩個資料要有一箇中間變數。如下虛擬碼:
int a = 3; int b = 10; //交換a和b的資料 int temp; temp = a;//第一步 a = b;//第二步 b = temp;//第三步
可以看到這樣的一次簡單換位要進行三步複製操作,如果陣列中物件很多,都要進行這種換位,那麼效率有點低,看看下面這個圖;
上圖進行三次這樣的交換就要經過3x3 = 9次複製操作,那有沒有方法可以優化一下呢?
下圖所示,用一個臨時節點儲存A節點,然後D、C、B依次複製到前面的位置,最後就將臨時節點放到原來D的位置,總共只需要進行5次複製,這樣減少了4次複製,在交換的次數很多的時候這種方式效率可還行。。。
4.java程式碼
根據上面說的我們用java程式碼來實現一下堆的新增和刪除操作,而且效率都是logN:
package com.wyq.test; public class MyHeap { //堆中的陣列要存節點,於是就是一個Node陣列 private Node[] heapArray; //陣列的最大容量 private int maxSize; //陣列中實際節點的數量 private int currentSize; public MyHeap(int size){ this.maxSize = size; this.currentSize = 0; this.heapArray = new Node[maxSize]; } //這裡為了方便使用,節點類就為一個靜態內部類,其中就存了一個int型別的資料,然後get/set方法 public static class Node{ private int data; public Node(int data){ this.data = data; } public int getData() { return data; } public void setData(int data) { this.data = data; } } //向堆中插入資料,這裡有幾點需要注意一下;首先,如果陣列最大的容量已經存滿了,那麼插入失敗,直接返回false; //然後,陣列沒有滿的話就將新插入的節點放在陣列實際存放資料的後一個位置 //最後就是向上調整,將新插入的節點和父節點交換,重複這個操作,直到放在合適的位置,調整完畢,陣列實際存放節點數量加一 //下面我們就好好看看向上調整的方法 public boolean insert(int value){ if (maxSize == currentSize) { return false; } Node newNode = new Node(value); heapArray[currentSize] = newNode; changeUp(currentSize); currentSize++; return true; } //向上調增 private void changeUp(int current) { int parent = (current-1)/2;//得到父節點的陣列下標 Node temp = heapArray[current];//將我們新插入的節點暫時儲存起來,這在前面交換那裡說過這種做法的好處 //如果父節點資料小於插入節點的資料 while(current>0 && heapArray[parent].getData()<temp.getData()){ heapArray[current] = heapArray[parent];//這裡相當於把父節點複製到當前新插入節點的這個位置 current = parent;//當前指標指向父節點位置 parent = (parent-1)/2;//繼續獲取父節點的陣列下標,然後又會繼續比較新插入節點資料和父節點資料,無限重複這個步驟 } //到達這裡,說明了該交換的節點已經交換完畢,換句哈來說就是已經把很多個節點向下移動了一個位置,留下了一個空位置,那就把暫時儲存的節點放進去就ok了 heapArray[current] = temp; } //刪除節點,邏輯比較容易,始終都是刪除根節點,然後將最後面一個節點放到根節點位置,然後向下調整就好了;最後就是將實際容量減一就可以了,重點看看向下調整 public Node delete(){ Node root = heapArray[0]; heapArray[0] = heapArray[currentSize-1]; currentSize--; changeDown(0); return root; } //向下調整,大概理一下思路,我們首先將新的根節點臨時儲存起來,然後要找兩個子節點中比較大的那個節點,最後就是比較臨時節點和比較大的節點的大小, //如果小,那麼把那個比較大的節點往上移動到父節點位置,繼續重複這個步驟將比較大的子節點往上移動,最後會留下一個空位置,就把臨時節點放進去就好 private void changeDown(int current) { Node largeChild; Node temp = heapArray[0];//臨時節點為新的根節點 //注意這個while迴圈的條件,current<currentSize/2可以保證當前節點至少有一個子節點 while (current<currentSize/2) { int leftIndex = 2*current+1;//左子節點陣列下標 int rightIndex = 2*current+2;//右子節點陣列下標 int largeIndex;//比較大的子節點陣列下標 Node leftChild = heapArray[leftIndex];//左子節點 Node rightChild = heapArray[rightIndex];//右子節點 if (rightIndex<currentSize && leftChild.getData()<rightChild.getData()) { largeChild = rightChild; largeIndex = rightIndex; }else { largeChild = leftChild; largeIndex = leftIndex; } //如果臨時節點(即新的根節點)比大的那個子節點還大,那麼就直接跳出迴圈 if (temp.getData() >= largeChild.getData()) { break; } heapArray[current] = largeChild; current = largeIndex;//當前節點的執著呢指向比較大的子節點 } heapArray[current] = temp;//臨時節點插入到堆陣列中 } //展示堆中的資料 public void display(){ System.out.print("堆中的資料為:"); for (int i = 0; i < currentSize; i++) { System.out.print(heapArray[i].getData()+" "); } } public static void main(String[] args) { MyHeap heap = new MyHeap(10); heap.insert(3); heap.insert(5); heap.insert(1); heap.insert(10); heap.insert(6); heap.insert(7); heap.display(); System.out.println(); System.out.print("刪除操作之後"); heap.delete(); heap.display(); } }
我們插入的節點如下圖所示:
程式碼測試結果為如下所示,成功;
5.總結
到這裡堆就差不多說完了,其實還有個堆排序,其實最簡單的就是向堆中新增很多節點,然後不斷的刪除節點,就會以從大到小的順序返回了,比較容易吧!當然還可以進行改進不過也很簡單,有興趣的可以自己去看看堆排序。
java資料結構到這裡差不多了,就隨意列舉一下我們用過的資料結構的時間複雜度:
&n