數組結構的缺點:java
1.數組的大小是固定的;算法
2.無序數組中,查找效率很低;查找O(N),插入O(1)數組
3.有序數組中,插入效率又很低;查找O(logN)使用二分法,提升查找效率,減小查找次數logN=log2(N)*3.322;插入O(N)數據結構
4.不論是哪一種數組,刪除操做效率都很低。O(N)學習
本章將學習單鏈表、雙端鏈表、有序鏈表、雙向鏈表和有迭代器的鏈表。大數據
在鏈表上,每個數據項,都被包含在一個「鏈結點」中。一個鏈結點是某個類的對象,這個類能夠叫作Link,Link類是鏈表類是分開的。每一個鏈結點對象,都包含一個對下一個鏈結點引用的字段(一般叫next)。可是鏈表自己的對象中有一個字段指向對第一個鏈結點的引用。this
咱們經過java代碼來建立Link類:spa
public class Link { private int iData; // int類型數據 private double dData; // double類型數據 private Link next; // 下一個link對象的應用,內存地址 }
單鏈表插入數據的過程:設計
單鏈表插入數據的JAVA代碼實現:code
package linkedlist; /** * @author yangjian * @date created in 11:34 2019/07/19 */ public class LinkList { private Link first; public void LinkList(){ first = null; } public boolean isEmpty(){ return (first == null); } public void insertFirst(int id, double dd){ Link newLink = new Link(id, dd); newLink.next = first; first = newLink; } public Link deleteFirst(){ Link temp = first; first = first.next; return temp; } public void displayLinkList(){ System.out.println("= displayLinkList begin :"); Link current = first; while(current != null){ current.displayLink(); current = current.next; } System.out.println("= displayLinkList end"); } } class LinkListApp{ public static void main(String [] args){ LinkList linkList = new LinkList(); linkList.insertFirst(1,10.0); linkList.insertFirst(2,20.0); linkList.insertFirst(3,30.0); linkList.insertFirst(4,40.0); linkList.insertFirst(5,50.0); linkList.displayLinkList(); } }
insertFirst()方法:
建立一個新的數據項newLink,準備插入單鏈表中。將新數據項的next指向表頭如今的數據項first,而後將新數據項替換表頭裏的first。這樣子表頭first的數據就更新爲新數據項,且新數據項的next指向了上一個first存儲的數據項。
deletaFirst()方法:
獲取如今的表頭項first,用於當作返回結果。獲取first.next數據項,插入表頭,實現把鏈表數據的更新。
查找和刪除指定鏈結點
find()方法:
public Link find(int key){ Link current = first; while(current.iData != key){ if(current.next == null){ return null; }else{ current = current.next; } } return current; }
從表頭結點開始判斷,若是表頭結點不匹配,則繼續從表頭結點的下一個結點開始判斷;直到全部結點找完判斷完畢,也沒有發現符合的結點;或者找到了匹配的結點爲止。
delete()方法:
public Link delete(int key){ Link current = first; Link previous = first; while(current.iData != key){ if(current.next == null){ return null; }else{ previous = current; current = current.next; } } if(current == first){ first = current.next; }else{ previous.next = current.next; } return current; }
建立兩個變量,一個存儲當前的結點,一個存儲當前結點的上一個結點;循環判斷,當前結點是否知足要求,若是不知足,則獲取當前結點的下一個結點繼續判斷,同時將當前不知足要求的結點,也存儲起來,若是下一個結點命中了,則須要將命中結點的next引用,賦值給父節點對象的next,這樣,命中的結點,就從鏈表中被移除了,而且父節點的next從指向命中結點,變動爲指向命中結點的next結點,鏈表沒有斷裂。最後判斷,若是要移除的結點是表頭結點first,則新的表頭結點爲first的next。
其餘方法:
好比insertAfter()方法,查找某個特定的關鍵結點,並在它的後面新建一個新結點。
雙端鏈表的數據結構:
雙端鏈表新增了一個特性:即增長了對最後一個鏈結點的引用。就像對錶頭結點的引用同樣,容許直接在表尾插入一個鏈結點,普通的鏈表也能夠實現這樣的功能,可是須要遍歷全部的結點,直到到達表尾的位置,效率很低,而雙端鏈表,提供了表頭和表尾兩個鏈結點的引用。注意,不要把雙端鏈表和雙向鏈表搞混。
雙端鏈表的java代碼實現:
package linkedlist; /** * @author yangjian * @date created in 16:24 2019/07/20 */ public class FirstLastList { private Link first; private Link last; public FirstLastList() { first = null; last = null; } public boolean isEmpty() { return (first == null); } public void insertFirst(int id, double dd) { Link newLink = new Link(id, dd); if (isEmpty()) { last = newLink; } newLink.next = first; first = newLink; } public void insertLast(int id, double dd) { Link newLink = new Link(id, dd); if (isEmpty()) { first = newLink; } else { last.next = newLink; } last = newLink; } public Link deleteFirst(int id) { if (isEmpty()) { return null; } Link temp = first; if (first.next == null) { last = null; } first = first.next; return temp; } public void displayLinkList(){ System.out.println("= displayLinkList begin :"); Link current = first; while(current!= null){ current.displayLink(); current = current.next; System.out.println(""); } System.out.println("= displayLinkList end"); } }
insertFirst()方法:
須要判斷鏈表是否爲空,爲空,第一次添加鏈結點,須要給last結點也賦值。
insertLast()方法:
須要判斷鏈表是否爲空,爲空,第一次添加鏈結點,須要給first結點也賦值。而且要保證,每次新加入的鏈結點,替換存儲在last引用上。
deleteFirst()方法:
須要判斷,若是刪除了當前 first鏈結點後,鏈表爲空了,須要將last結點也賦值爲null
deleteLast()方法:
雙端鏈表在由於沒有存儲last鏈結點的父引用結點,因此再實現移除表尾結點的實現上,須要遍歷整個鏈表,找出表尾結點的父引用結點,將父引用結點賦值到last上,效率很低。這裏沒有實現,後面再使用雙向鏈表時,會討論到這一點。
在表頭插入和刪除的速度很快,僅須要修改一兩個引用值,隨意花費時間是O(1)。
平均起來,查找、刪除和在指定的鏈結點後面插入都須要搜索表中一半的鏈結點,須要O(N)次比較。在數組中執行這些操做也是O(N)次比較,可是鏈表仍然要快一些,由於當插入和刪除結點時,鏈表數據不須要移動。增長的效率顯著,特別是在複製時間遠遠大於比較時間的時候。
鏈表比數組的另外一一個優點是,鏈表不須要像數組同樣指定容量,鏈表須要多少容量就能夠擴展多少容量。數組常常由於容量太大,致使效率低下,或者容量過小,而致使內容溢出。
在上一篇文章中,介紹了棧和隊列這樣的數據結構,並經過數組來實現了棧和隊列對數據項的操做。
如今咱們學習了鏈表,那麼如何使用鏈表實現棧和隊列的數據結構呢?
鏈表實現棧:只須要保留insertFirst()和deleteFirst()方法便可,每次插入數據項和刪除數據項,都對鏈表的表頭操做便可。
鏈表實現隊列:只須要保留insertLast()和deleteFirst()方法便可,每次向鏈表的表尾插入數據項,並每次從鏈表的表頭去除數據項便可。
數組實現棧和隊列,須要維護下標;而鏈表只須要維護表頭和表尾便可。
有序鏈表,就是在插入新的數據項/鏈結點時,根據某個關鍵詞作排序,使鏈表的鏈結點擁有先後順序。
有序鏈表的java代碼實現:
package linkedlist; /** * @author yangjian * @date created in 17:46 2019/07/20 */ public class SortedList { private SortedLink first; public SortedList() { first = null; } public boolean isEmpty() { return (first == null); } public void insert(long key) { SortedLink newLink = new SortedLink(key); SortedLink current = first; SortedLink previous = null; while (current != null && current.dData > key) { previous = current; current = current.next; } if (previous == null) { first = newLink; } else { previous.next = newLink; } newLink.next = current; } public SortedLink remove() { SortedLink temp = first; first = first.next; return temp; } public void displayLinkList() { System.out.println("= displayLinkList begin :"); SortedLink current = first; while (current != null) { current.display(); current = current.next; System.out.println(""); } System.out.println("= displayLinkList end"); } } class SortedLink { public long dData; public SortedLink next; public SortedLink(long dData) { this.dData = dData; next = null; } public void display() { System.out.print("{" + dData + "}"); } } class SortedLinkApp { public static void main(String[] args) { SortedList theSortedList = new SortedList(); theSortedList.insert(10); theSortedList.insert(30); theSortedList.insert(20); theSortedList.insert(40); theSortedList.insert(50); theSortedList.displayLinkList(); theSortedList.remove(); theSortedList.displayLinkList(); } }
最重要的方法是insert()方法
有序鏈表的效率
有序鏈表插入一個數據項,最多須要O(N)次比較,平均是(N/2),跟數組同樣。可是在O(1)的時間內就能夠找到並刪除表頭的最小/最大數據項。若是一個應用頻繁的存儲最小項,且不須要快速的插入,那麼有序鏈表時一個有效的方案。優先級隊列就可使用有序鏈表來實現。
如何給一個無序數組排序?
如今有一個無序數組,若是要給無序數組進行排序,可使用數組的插入排序法,可是插入排序法的時間級爲O(N的2次方)(使用了雙層循環的時間級別,就是N的2次方)。這個時候,咱們能夠建立一個有序鏈表,將無序數組的數據項,挨個取出,插入到有序鏈表中,由有序鏈表實現數據項的排序,再從有序鏈表取出數據項從新插回數組中,就是排序後的結果。這樣作的好處是,大大減小移動次數,在數組中進行插入排序須要N的2次方移動;而是用有序鏈表,數據項一次從數組到鏈表,一次從鏈表到數組,相比之下2*N次移動更好。
雙向鏈表和雙端鏈表是不同的,由於單鏈表和雙端鏈表,經過current.next能夠很方便的到達下一個鏈結點,可是反向的遍歷就很困難。雙向鏈表提供了這個能力,即容許向後遍歷,也容許向前遍歷。其中的祕密就是每一個鏈結點,有兩個指向其餘鏈結點的引用,而不是一個。
public class Link { public long dData; public Link next; // 下一個link對象的應用,內存地址 public Link previous; // 上一個link對象的應用,內存地址 }
雙端鏈表的意思是,鏈表中維護表頭和表尾兩個引用,由於頗有用,因此在雙向鏈表中,也能夠保留雙端鏈表的特性。
遞歸的三個要素:
1.調用本身
2.每次調用本身是爲了解決一個更小的問題
3.存在一個基值Base case/限制條件,當知足條件時,直接返回結果
遞歸中必須存在限制條件,若是沒有限制條件,會形成一種算法中的龐氏騙局,永遠沒法結束。
三角數:1,3,6,10,15,21.....第N項等於第N-1項加N,第n個三角數字=(n的2次方+n)/2。
三角數表達遞歸:
int trianle(int n){ if(n==1){ return 1; }else{ int temp = n + trianle(n-1); return temp; } }
遞歸的效率:咱們使用遞歸,是由於遞歸從概念上簡化了問題,而不是由於遞歸真的能夠提升效率。咱們調用一個方法時,在內存上會爲方法生成一個棧空間,當這個方法使用遞歸的時,會在棧內存中一直調用新的方法,若是這個方法的數據量很大,那麼會容易引發棧內存溢出的問題。
數學概括法:
遞歸就是程序設計中的數學概括法。數學概括法就是一種經過自身的語彙定義某事物本身的方法。
tri(n) = 1 if n = 1
tri(n) = n + tri(n-1) if n > 1
階乘:階乘與三角數同樣,三角數中第n項的數值等於n加上第n-1項的三角數;而階乘中,第n項的數值等於n乘以第n-1項的階乘,即第5項數值的階乘等於5*4*3*2*1=120。
0的階乘被定義爲1
咱們先回顧一下基於有序數組的二分查找方法如何實現的:
package sorte; /** * @author yangjian * @date created in 18:47 2019/07/22 */ public class SortedErFenFa { private int nItems = 10; private long[] arr = new long[]{1,2,3,4,5,6,10,14,24,35}; public int find(long searchKey){ int lowIndex = 0; int upperIndex = arr.length - 1; int currentIndex; while(true){ // 每次獲取比較範圍的中間位置的變量下標 currentIndex = (lowIndex + upperIndex)/2; // (0 + 9)/2 = 4 // 命中 if(arr[currentIndex] == searchKey){ return currentIndex; // 若是傳入的數據項,在數組中介於兩個相連的元素之間,可是數組中缺不存在, // lowIndex自己是小於upperIndex的,可是隨着循環次數的增長,lowIndex會等於upperIndex, // 最後lowIndex會大於currentIndex }else if(upperIndex < lowIndex){ return nItems; }else{ // 中間數大於入參,縮小範圍爲前半截 if(arr[currentIndex] > searchKey){ upperIndex = currentIndex - 1; // 中間數小於入參,縮小範圍爲後半截 }else if(arr[currentIndex] < searchKey){ lowIndex = currentIndex + 1; } } } } }
遞歸取代循環:上述方法還能夠用遞歸來實現
package recursion; /** * @author yangjian * @date created in 19:12 2019/07/22 */ public class SortedErFenFa { private int nItems = 10; private long[] arr = new long[]{1,2,3,4,5,6,10,14,24,35}; public int recFind(long searchKey, int lowerIn, int upperIn){ int curIn; curIn = (lowerIn + upperIn)/2; if(arr[curIn] == searchKey){ return curIn; }else if(lowerIn > upperIn){ return nItems; }else{ if(arr[curIn] > searchKey){ return recFind(searchKey, lowerIn, curIn + 1); }else{ return recFind(searchKey, curIn - 1, upperIn); } } } }
有序數組的insert方法:
public void insert(long value) { int j; // 找到value應該插入的下標 for (j = 0; j < nItems; j++) { if (arr[j] > value) { break; } } // 給數組擴容一位,並將大於j下標的元素,向右移動 for (int k = nItems; k > j; k--) { arr[k] = arr[k - 1]; } // 將value插入到數組中 arr[j] = value; // 數組容量+1 nItems++; }
二分查找法,是分治算法的一個例子,把大問題拆分爲兩個更小的問題,而後對待每個小問題的解決辦法也是同樣的:把每一個小問題拆分爲兩個更小的問題,並最終解決它們。這個過程持續下去,直到達到求解的基值狀況,就不用了再拆分了。
分治法一般要用到遞歸。一般是一個方法,含有兩個對自身的調用,分別對應於問題的兩個部分。在二分查找法中,也有兩個遞歸的調用,可是隻有一個是真的執行了,後面咱們遇到的歸併排序,它是真正的執行了兩個遞歸調用(對分組後的兩部分數據分別排序)。
冒泡排序,插入排序和選擇排序要用O(N的2次方)時間,而歸併排序只要O(N*logN)。若是N是10000條數據,那麼N的2次方就是100000000,而N*logN只是10000*4=40000,若是歸併排序須要40秒,那麼插入排序就須要將近28小時。
歸併排序的一個缺點是須要在存儲器中有另外一個大小等於被排序的數據項數目的數組。若是初始數組幾乎佔滿了整個存儲器,那麼歸併排序將不能工做,可是空間足夠的話,歸併排序是一個很好的選擇。
歸併兩個有序數組
歸併排序的中心是歸併兩個已經有序的數組A和B,就生成了數組C,數組C包含數組A和B全部的數據項,而且使它們有序的排列在C中。
非歸併排序實現上圖排序;
package sort; /** * @author yangjian * @date created in 09:52 2019/07/23 */ public class MergeApp { public static void main(String [] args){ int[] a = {1,4,5,7,29,45}; int[] b = {2,6,33,56}; int[] c = new int[10]; merge(a, a.length, b, b.length, c); display(c); } public static int[] merge(int[] a, int aSize, int[] b, int bSize, int[] c){ int aIn = 0; int bIn = 0; int cIn = 0; while(aIn < aSize && bIn < bSize){ if(a[aIn] <= b[bIn]){ c[cIn++] = a[aIn++]; }else{ c[cIn++] = b[bIn++]; } } while(aIn < aSize){ c[cIn++] = a[aIn++]; } while(bIn < bSize){ c[cIn++] = b[bIn++]; } return c; } public static void display(int[] c){ for(int i = 0; i < c.length; i++){ System.out.print(c[i] + " "); } } } //輸出結果:1 2 4 5 6 7 29 33 45 56
經過歸併排序實現排序: