【算法】數據結構與算法基礎總覽(上)數據結構篇

前言html

       對於絕大多少程序員來講,數據結構與算法絕對是一門很是重要但又很是難以掌握的學科。最近本身系統學習了一套數據結構與算法的課程,也開始到Leetcode上刷題了。這裏對課程中講到的一些數據結構與算法基礎作了一些回顧和總結,從宏觀上先來了解整個知識框架。java

 

數據結構與算法總覽圖程序員

一、數組(Array)算法

       數組的底層硬件實現是,有一個叫內存控制器的結構,爲數組分配一個段連續的內存空間,這些空間中存儲着數組中對應的值(值爲基本數據類型)或者地址(值爲引用類型)。當根據index訪問數組中的某個元素時,內存控制器直接定位到該index所在的地址,不管是第一個元素、中間元素仍是最後一個元素,都能一次性定位到,時間複雜度爲O(1)。api

        Java中ArrayList是對數組的一個典型使用,其內部維護着一個數組,ArrayList的增、刪、查等,都是對其中數組進行操做。因此根據index進行查找時比較快,時間複雜度爲O(1);但增長和刪除元素時須要擴容或者移動數組元素的位置等操做,其中擴容時還會開闢更大容量的數組,將原數組的值複製到新數組中,並將新數組複製給原數組,因此此時時間複雜度和空間複雜度爲O(n)。對於頻繁查找數據時,使用ArrayList效率比較高。數組

        ArrayList源碼:http://developer.classpath.org/doc/java/util/ArrayList-source.html緩存

 

二、鏈表(Linked List)安全

       能夠經過使用雙向鏈表或者設置頭尾指針的方式,讓操做鏈表更加方便。網絡

       Java中LinkedList是對鏈表的一個典型使用,其內部維護着一個雙向鏈表,對數據的增,刪、查、改操做實際上都是對鏈表的操做。增、刪、改非首位節點自己操做時間複雜度爲O(1),可是要查找到對應操做的位置,實際上也要通過遍歷查找,而鏈表的時間複雜度爲O(n)。數據結構

       LinkedList源碼:http://developer.classpath.org/doc/java/util/LinkedList-source.html

       參考閱讀:http://www.javashuo.com/article/p-cctdnsmh-y.html

 

三、跳錶(Skip List)

       跳錶是在一個有序鏈表的基礎上升維,添加多級索引,以空間換時間,其空間複雜度爲O(n),用於存儲索引節點。其有序性對標的是平衡二叉樹,二叉搜索樹等數據結構。

        

數組、鏈表、跳錶對增、刪、查時間複雜度比較:

  數組 鏈表 跳錶
preppend O(n) O(1) O(logn)
append O(1) O(1) O(logn)
lookup O(1) O(n) O(logn)
insert O(n) O(1) O(logn)
delete O(n) O(1) O(logn)

 

四、棧(Stack)

     Java中雖然提供了Stack類(內部維護的實際上也是一個數組)用於實現棧,但官方文檔 https://www.apiref.com/java11-zh/java.base/java/util/Stack.html中明確說明,應該優先使用Deque來實現:

     Deque<Integer> stack = new ArrayDeque<Integer>();

     Deque接口及其實現,提供了一套更完整和一致的LIFO(Last in first out ,後進先出)堆棧操做,這裏列舉幾個用於棧的方法:

     public E peek():檢索但不移除此雙端隊列表示的隊列的頭部(換句話說,此雙端隊列的第一個元素),若是此雙端隊列爲空,則返回 null 。

     public E pop:今後雙端隊列表示的堆棧中彈出一個元素。

     public void push(E e):在不違反容量限制的狀況下執行此操做, 能夠添加元素到此雙端隊列表示的堆棧(換句話說,在此雙端隊列的頭部),若是當前沒有可用空間則拋出 IllegalStateException 。

     ArrayDeque實現類中,實際上也是維護的一個數組,下面會對該類作進一步介紹。

 

五、隊列(Queue)

       Java中提供了實現接口Queue,源碼爲:http://fuseyism.com/classpath/doc/java/util/Queue-source.html ;參考文檔:https://www.apiref.com/java11-zh/java.base/java/util/Queue.html。Java還提供了不少實現類,好比ArrayDeque、LinkedList、PriorityQueue等,可使用以下方式來使用Queue接口:

       Queue<String> queue = new LinkedList<String>();

       Queue接口針對入隊、出隊、查看隊尾操做提供了兩套API:

           第一套爲,boolean add(E e) 、E element()、E remove(),在超過容量限制或者獲得元素爲null時,會報異常。

           第二套爲,boolean offer(E e)、E peek()、E poll(),不會報異常,而是返回true/false/null,通常工程中使用這一套api。

       實現類LinkedList中實際維護的是一個雙向鏈表,前面已經介紹過了。

 

六、雙端隊列(Deque)

      Deque是Double end queue的縮寫,參考文檔:https://www.apiref.com/java11-zh/java.base/java/util/Deque.html

      Deque既提供了用於實現Stack LIFO的push、pop、peek,又提供了用於實現Queue FIFO(First In First Out:先進先出)的offer、poll、peek(add、remove、element等也有,這裏僅列出推薦使用的),因此能夠用於實現Stack,也能夠用於實現Queue。同時,Deque還提供了全新的接口用於對應Stach和Queue的方法,如offerFirst/offerLast、peekFirst/peekLast,pollFirst/pollLast等,另外還提供了一個addAll(Collection c),批量插入數據。

       前面講Stack的時候已經介紹過了,Deque是一個接口,通常在工程中的使用方式爲:

       Deque<Integer> deque = new ArrayDeque<Integer>();

       ArrayDeque內部維護的是一個數組。

 

七、優先隊列(PriorityQueue)

       Java中提供了PriorityQueue類來實現優先隊列,是接口Queue的實現類。和Queue的FIFO不一樣的是,PriorityQueue中的元素是按照必定的優先級排序的。默認狀況下,其內部是經過一個小頂堆來實現排序的,也能夠自定義排序的優先級。堆是一個徹底二叉樹,能夠用數組來表示,因此PriorityQueue內部其實是維護的一個數組。

       PriorityQueue提供了對隊列的基本操做:offer用於向堆中插入元素,插入後會堆會進行調整;peek用於查看但不刪除數組的第一個元素,也就是堆頂的元素,是優先級最高(最大或者最小)的元素;poll用於獲取並移除堆頂元素,堆會再進行調整。固然,對應的還有add/element/remove方法,這在前面Queue部分講過了。

       官方文檔:https://docs.oracle.com/javase/10/docs/api/java/util/PriorityQueue.html

       參考閱讀:https://blog.csdn.net/u010623927/article/details/87179364

 

八、哈希表(Hash Table)

         哈希表也叫散列表,經過鍵值對key-value的方式直接進行存儲和訪問的數據結構。它經過一個映射函數將Key映射到表中的對應位置,從而能夠一次性找到對應key-value結點的位置。

         Java中提供了HashTable、HashMap、ConcurrentHashMap等類來實現哈希表,這三者也常常被拿來作比較,這裏簡單介紹一下這三個類:

         HashTable:1)內部經過數組 + 單鏈表實現;2)主要方法都加了Synchronized鎖,線程安全,但也是由於加了鎖,因此效率比其它兩個差;3)Key和Value均不容許爲null;

HashTable內部結構圖      

         HashMap:1)Jdk1.7及以前,內部經過數組 + 單鏈表實現;Jdk1.8開始,內部經過 數組 + 單鏈表 + 紅黑樹實現 ;2)非線程安全,若是要保證線程安全,通常經過 Map m = Collections.synchronizedMap(new HashMap(...));的方式來實現,因爲沒有加鎖,因此HashMap效率比較高;3)容許一個Key爲null,Value也能夠爲null。

       ConcurrentHashMap:分段加鎖,相比HashMap它是線程安全的,相比HashTable它效率高,能夠當作是對HashMap和HashTable的綜合。

HashMap內部結構圖

 

九、映射(Map)

       映射中是以鍵值對Key-Value的形式存儲元素的,其中Key不容許重複,但Value能夠重複。Java中提供了Map接口來定義映射,還提供瞭如HashMap、ConcurrentHashMap等實現類,這兩個類前面有簡單介紹過。

 

十、集合(Set)

       集合中不容許有重複的元素,添加元素時若是有重複,會覆蓋掉原來的元素。Java中提供了Set接口來定義集合,也提供了HashSet實現類。HashSet類的內部實際上維護了一個HashMap,將添加的對象做爲HashMap的key,Object對象做爲value,以此來實現集合中的元素不重複。

 1 //HashSet部分源碼
 2 public class HashSet<E>  extends AbstractSet<E>  implements Set<E>, Cloneable, java.io.Serializable {
 3     ......
 4     private transient HashMap<E,Object> map;
 5     private static final Object PRESENT = new Object();
 6     public HashSet() {
 7         map = new HashMap<>();
 8     }
 9     ......
10     public boolean add(E e) {
11         return map.put(e, PRESENT)==null;
12     }
13     ......
14     public boolean remove(Object o) {
15         return map.remove(o)==PRESENT;
16     }
17     ......
18 }

 

十一、樹(Tree)

       在單鏈表的基礎上,若是一個節點的next有一個或者多個,就構成了樹結構,因此單鏈表是一棵特殊的樹,其child只有一個。關於樹有不少特定的結構,用於平時的工程中,出現得比較多得就是二叉樹,而二叉樹根據用途和性質又有多種類型,常見的有:

       徹底二叉樹:若設二叉樹的深度爲k,除第 k 層外,其它各層 (1~k-1) 的結點數都達到最大個數,第k 層全部的結點都連續集中在最左邊,這就是徹底二叉樹。徹底二叉樹能夠按層存儲在數組中,若是某個結點的索引爲i,那麼該結點若是有左右結點,那麼左右結點的索引分別爲2i+1,2i+2;

        滿二叉樹:一個二叉樹,若是每個層的結點數都達到最大值,則這個二叉樹就是滿二叉樹。也就是說,若是一個二叉樹的層數爲K,且結點總數是(2^k) -1 ,則它就是滿二叉樹。因此,滿二叉樹也是徹底二叉樹。

     

        二叉搜索樹(Binary Search Tree):又稱爲二叉排序樹、有序二叉樹、排序二叉樹等。其特徵爲:任意一個結點的左子樹的值都小於/等於該結點的值,右子樹的值都大於/等於根結點的值;中序遍歷的結果是一個升序數列;任意一個結點的左右子樹也是二叉搜索樹。以下圖所示:

       在極端的狀況下,二叉搜索樹會呈一個單鏈表。

       平衡二叉樹(AVL):它或者是一顆空樹,或它的左子樹和右子樹的深度之差(平衡因子)的絕對值不超過1,且它的左子樹和右子樹都是一顆平衡二叉樹。平衡二叉樹也是一棵二叉搜索樹,因爲具備平衡性,因此整棵樹比較平衡,不會出現一長串單鏈表的結構,在查找時最壞的狀況也是O(logn)。爲了保持平衡性,每次插入的時候都須要調整以達到平衡。

        以下圖所示,任意一個結點的左右子樹的深度差絕對值都不超過1,且符合二叉搜索樹的特色:

十二、紅黑樹

       紅黑樹是一顆平衡二叉搜索樹,具備平衡性和有序性,結點的顏色爲紅色或者黑色。這裏的「平衡」和平衡二叉樹的「平衡」粒度上不一樣,平衡二叉數更爲嚴格,致使在插入或者刪除數據時調整樹結構的頻率過高了,這會致使必定的性能問題。而紅黑樹的平衡是任意一個結點的左右子樹,較高的子樹與較低子樹之間的高度差不超過兩倍,這樣就能從必定層度上避免過於頻繁調整結構。能夠認爲紅黑樹是對平衡二叉樹的一種變體。

1三、圖(Graph)

      單鏈表是特殊的樹,樹是特殊的圖。

 

1四、堆(Heap)

       堆是一種能夠迅速找到最大值或者最小值的數據結構,內部維護着一棵樹(注意這裏說的是樹,而不是限制於二叉樹,也能夠是多叉)。若是該堆的根結點是最大值,則稱之爲大頂堆(或大根堆);若是根結點是最小值,則稱爲小頂堆(或小根堆)。堆的實現有不少,這裏主要介紹一下二叉堆。

       二叉堆,顧名思義,就是堆中的樹是一棵二叉樹,且是徹底二叉樹(這裏要注意區別於二叉搜索樹),因此能夠用數組表示,前面介紹的PriorityQueue就是一個堆的實現。若是是大頂堆,任何一個結點的值都 >= 其子結點的值大;若是是小頂堆,則任何一個結點的值都 <= 其子節點的值。下圖展現了一個二叉大頂堆,其對應的一維數組爲[110, 100, 90, 40, 80, 20, 60, 10, 30, 50, 70]:

       對於大頂堆而言,通常常使用的操做是查找最大值、刪除最大值和插入一個值,其時間複雜度分別爲:查找最大值的時間複雜度是O(1),由於最大值就是根結點的值,位於數組的第一個位置;刪除最大值,找到最大值的時間複雜度是O(1),可是刪除後該堆須要從新調整,將最底層最末尾的結點移到根結點,而後根節點再與子結點點比較,並與較大的結點交換,直到該結點不小於子結點爲止,因爲是從最末尾的結點直接升到根結點,因此該結點的值確定是相對很小的,須要調整屢次才能再次符合堆的定義,因此時間複雜度爲O(logn);插入一個結點,其作法是在數組的最後插入,也就是二叉樹的最後一個層的末尾位置插入,而後再和其父結點比較,若是新結點大就和父結點交換位置,直到不大於根結點爲止,因此插入新的結點可能一次到位,時間複雜度爲O(1),也有可能還須要調整,最壞的時候比較和交換O(logn),即時間複雜度爲O(logn)。同理,小頂堆也是如此。

       堆的實現代碼參考:https://shimo.im/docs/Lw86vJzOGOMpWZz2/read

 

1五、並查集(Disjoint Set)

      並查集通常用於解決元素,組團或者配對的問題,便是否在一個集合的問題。它管理着一系列不相交的集合,主要提供以下三種基本操做:

    (1)makeSet(s),建立並查集:建立一個新的並查集,其中包含s個單元素集合;

    (2)unionSet(x,y),合併集合:將x元素和y元素所在的集合不相交,就將這兩個集合合併;若是這兩個結合相交,則不合並;

    (3)find(x),查找表明:查找x元素所在集合的表明元素,該操做能夠用於判斷兩個元素是否在同一個集合中,若是兩個元素的表明相同,表示在同一個集合;不然,不在同一個集合。

       若是想避免並查集過高,還能夠進行路徑壓縮。

       實現並查集的基本代碼模板:

 1 public class UnionFind {
 2     private int count = 0;
 3     private int[] parent;
 4 
 5     //初始化並查集,用數組存儲每一個元素的父節點,一開始將他們的父節點設爲本身
 6     public UnionFind(int n) {
 7         count = n;
 8         parent = new int[n];
 9         for (int i = 0; i < n; i++) {
10             parent[i] = i;
11         }
12     }
13 
14     //找到元素x所在集合的表明元素
15     public int find(int x) {
16         while (x != parent[x]) {
17             x = parent[x];
18         }
19         return x;
20     }
21 
22     //合併x和y所在的集合
23     public void union(int x, int y) {
24         int rootX = find(x);
25         int rootY = find(y);
26         if (rootX == rootY)
27             return;
28         parent[rootX] = rootY;
29         count--;
30     }
31 }

       這裏推薦一篇寫不錯的文章:https://www.cnblogs.com/noKing/p/8018609.html

 

1六、字典樹(Trie)

       字典樹,即Trie樹,又稱爲前綴樹、單詞查找樹或者鍵樹,是一種樹形結構。Trie的優勢是最大限度地減小無畏的字符串比較,查詢效率比hash表高。其典型應用是統計字符串(不限於字符串)出現的頻次,查找具備相同前綴的字符串等,因此常常被搜索引擎用於統計單詞頻次,或者關鍵字提示,以下圖所示:

Trie樹具備以下特性:

  (1)結點自己不存儲完整單詞;

  (2)從根結點到某一結點,路徑上通過的字符串聯起來,對應的就是該結點表示的字符串;

  (3)每一個結點全部的子結點路徑表明的字符都不相同。

       實際工程中,結點能夠存儲一些額外的信息,以下圖就表示一棵Trie樹,每一個結點存儲了其對應表示的字符串,以及該字符串被統計的頻次。

     對於一個僅由26個小寫英文字母組成的字符串造成的Trie樹,其結點的內部結構爲:

 Trie樹的核心思想是以空間換時間,由於須要額外建立一棵Trie樹,它利用字符串的公共前綴來下降查詢的時間的開銷從而提高效率。

 

1七、布隆過濾器(Bloom Filter)

     布隆過濾器典型應用有,垃圾郵件/評論過濾、某個網址是否被訪問過等場景,它是由一個很長的二進制向量和一系列的hash函數實現的,其結構以下圖所示:

一個元素A通過多個hash函數(本例中是兩個)計算後獲得多個hash code,在向量表中code對應的位置的值就設置爲1。

其具備以下特色:

 (1)存儲的信息是比較少的,不會存儲整個結點的信息,相比於HashMap/HashTable而言,節約了大量的空間;

 (2)若是判斷某個元素不存在,則必定不存在;

 (3)具備必定的誤判率,並且插入的元素越多,誤判率越過,若是判斷某個元素存在,那隻能說可能存在,須要再作進一步的判斷,因此稱爲過濾器;

 因此,其優勢是空間效率和查詢時間都遠遠優於通常的算法;缺點是具備必定的誤判率,且刪除元素比較困難(向量表中每個位置可能對應着衆多元素)。

    參考閱讀:https://baike.baidu.com/item/bloom%20filter/6630926?fr=aladdin

 

1八、LRU Cache 

        LRU,即Least Recently Used,最近最少使用,應用很是普遍,在Android的網絡圖片加載工具ImageLoader等中都具備使用。其思想爲,因爲空間資源有限,當緩存超過指定的Capacity時,那些最近最少使用的緩存就會被刪除掉,其工做機制以下圖所示:

        不一樣的語言中都提供了相應的類來實現LRU Cache,Java中提供的類爲LinkedHashMap,內部實現思想爲HashMap + 雙向鏈表。咱們也能夠經過HashMap + 雙向鏈表本身實現一個LRU Cache。

 1 //空間複雜度O(k),k表示容量
 2 //小貼士:在雙向鏈表的實現中,使用一個僞頭部(dummy head)和僞尾部(dummy tail)標記界限,這樣在添加節點和刪除節點的時候就不須要檢查相鄰的節點是否存在。
 3 class LRUCache {
 4     HashMap<Integer, LNode> cache = new HashMap<>();//使用hashmap能夠根據key一次定位到value
 5     int capacity = 0;//容量
 6     int size = 0;
 7     //採用雙鏈表
 8     LNode head;
 9     LNode tail;
10 
11     public LRUCache(int capacity) {
12         this.capacity = capacity;
13         //初始化雙鏈表
14         head = new LNode();
15         tail = new LNode();
16         head.next = tail;
17         tail.prev = head;
18     }
19 
20     //時間複雜度:O(1)
21     public int get(int key) {
22         //先從緩存裏面查,不存在返回-1;存在則將該節點移動到頭部,表示最近使用過,且返回該節點的value
23         LNode lNode = cache.get(key);
24         if (lNode == null) return -1;
25         moveToHead(lNode);
26         return lNode.value;
27     }
28 
29     //時間複雜度O(1)
30     public void put(int key, int value) {
31         LNode lNode = cache.get(key);
32         //若是hashmap中不存在該key
33         if (lNode == null) {
34             size++;
35             //若是已經超過容量了,須要先刪除尾部節點,且從hashmap中刪除掉該元素
36             if (size > capacity) {
37                 cache.remove(tail.prev.key);
38                 removeNode(tail.prev);
39                 size--;
40             }
41             //將新的節點存入hashmap,並添加到鏈表的頭部
42             lNode = new LNode(key, value);
43             cache.put(key, lNode);
44             addToHead(lNode);
45         } else {
46             //若是hashmap中存在該key,則修改該節點的value,且將該節點移動到頭部
47             lNode.value = value;
48             removeNode(lNode);
49             addToHead(lNode);
50         }
51     }
52 
53     /**
54      * 將節點移動到頭部
55      */
56     public void moveToHead(LNode lNode) {
57         removeNode(lNode);
58         addToHead(lNode);
59     }
60 
61     /**
62      * 移除節點
63      */
64     public void removeNode(LNode lNode) {
65         lNode.prev.next = lNode.next;
66         lNode.next.prev = lNode.prev;
67         lNode.next = null;
68         lNode.prev = null;
69     }
70 
71     /**
72      * 在頭部添加節點
73      */
74     private void addToHead(LNode lNode) {
75         head.next.prev = lNode;
76         lNode.next = head.next;
77         head.next = lNode;
78         lNode.prev = head;
79     }
80 }
81 
82 class LNode {
83     int key;
84     int value;
85     LNode prev;
86     LNode next;
87 
88     public LNode() {
89     }
90 
91     public LNode(int key, int value) {
92         this.key = key;
93         this.value = value;
94     }
95 }

 

最後

       最後附上一張常見數據結構的時間和空間複雜度表

相關文章
相關標籤/搜索