學好數據結構和算法 —— 線性表

線性表

  線性表表示一種線性結構的數據結構,顧名思義就是數據排成像一條線同樣的結構,每一個線性表上的數據只有前和後兩個方向。好比:數組、鏈表、棧和隊列都是線性表,今天咱們分別來看看這些線性數據結構。java

數組

數組是一種線性表數據結構,用一組連續的內存空間來存儲一組具備相同類型的數據。node

內存分佈:git

隨機訪問

連續內存空間存儲相同類型的數據,這個特性支持了數組的隨機訪問特性,相同類型的數據佔用的空間是固定的假設爲data_size,第n個元素的地址能夠根據公式計算出來:github

&a[n] = &a[0] + n * data_size編程

其中:數組

&a[n]:第n個元素的地址安全

&a[0]:第0個元素的地址(數組的地址)數據結構

data_size:數組存儲的元素的類型大小併發

因此訪問數組裏指定下標的任何一個元素,均可以直接訪問對應的地址,不須要遍歷數組,時間複雜度爲O(1)。編程語言

插入/刪除低效

爲了保證內存的連續性,插入或刪除數據時若是不是在數組末尾操做,就須要作數據的搬移工做,數據搬移會使得數組插入和刪除時候效率低下。

向數組裏插入一個元素有三種狀況:

一、向末尾插入一個元素,此時的時間複雜度爲O(1),對應最好時間複雜度。

二、向數組開頭插入一個元素,須要將全部元素依次向後挪一個位置,而後將元素插入開頭位置,此時時間複雜度爲O(n),對應最壞時間複雜度。

三、向數組中間位置插入一個元素,此時每一個位置插入元素的機率都是同樣的爲1/n,平均複雜度爲(1+2+3+…+n)/n = O(n)

若是數組元素是沒有順序的(或不須要保證元素順序),向數組中間位置插入一個元素x,只須要將插入位置的元素放到數組的末尾,而後將x插入,這時候不須要搬移元素,時間複雜度仍爲O(1),如:

 

一樣地,刪除數據的時候,從開頭刪除,須要將後面n-1個元素往前搬移,對應的時間複雜度爲O(n);從末尾刪除時間複雜度爲O(1);平均時間複雜度也是O(n)。

若是不須要保證數據的連續性,有兩種方法:

一、能夠將末尾的數據搬移到刪除點插入,刪除末尾那個元素

二、刪除的時候作標記,並不正真刪除,等數組空間不夠的時候,再進行批量刪除搬移操做

Java的ArrayList

  不少編程語言都針對數組進行了封裝,好比Java的ArrayList,能夠將數組的不少操做細節封裝起來(插入刪除的搬移數據或動態擴容),能夠參考ArrayList的擴容數據搬移方法,ArrayList默認size是10,若是空間不夠了會先按1.5倍擴容(若是還不夠就可能會用到最大容量)。因此在使用的時候若是事先知道數組的大小,能夠一次性申請,這樣能夠免去自動擴容的性能損耗。

何時選擇使用編程語言幫咱們封裝的數組,何時直接使用數組呢?

一、Java ArrayList不支持基本數據類型,須要封裝爲Integer、Long類才能使用。Autoboxing、Unboxing有必定的性能消耗。若是比較關注性能能夠直接使用數組

二、使用前已經能確認數據大小,而且操做比較簡單可使用數組

鏈表

  比起數組,鏈表不須要連續內存空間,它經過指針將一組分別獨立的內存空間串起來造成一條「鏈條」。鏈表有不少種,如:單鏈表、雙向鏈表、循環鏈表和雙向循環鏈表。

  鏈表內存分佈:

單鏈表

以下圖所示,鏈表的每一項咱們稱爲結點(node),爲了將全部結點鏈接起來造成鏈表,結點除了須要記錄數據以外,還須要記錄下一個結點的地址,記錄下一個結點地址的指針咱們叫作後繼指針(next),第一個結點和最後一個結點比較特殊,第一個結點沒有next指針指向它,稱爲頭結點,最後一個結點next指針沒有指向任何元素,稱爲尾結點。頭結點用來記錄鏈表的基地址,有了它就能夠順着鏈子往下搜索每個元素,若是遇到next指向null表示到達了鏈表的末尾。

在數組裏插入或刪除數據須要保證內存空間的連續性,須要作數據的搬移,可是鏈表裏數據內存空間是獨立的,插入刪除只須要改變指針指向便可,因此鏈表的插入刪除很是高效。如圖:

雙向鏈表

  單向鏈表每一個結點只知道本身下一個結點是誰,是一條單向的「鏈條」,而在雙向鏈表裏每一個結點既知道下一個結點,還知道前一個結點,相比單鏈表,雙向鏈表每一個結點多了一個前驅指針(prev)指向前一個結點的地址,以下圖所示:

由於每一個結點要額外的空間來保存前驅結點的地址,因此相同數據狀況下,雙向鏈表比單鏈表佔用的空間更多。雙向鏈表在找前驅結點時間複雜度爲O(1),插入刪除都比單鏈表高效,典型的空間換時間的例子。

循環鏈表

  將一個單鏈表首尾相連就造成了一個環,這就是循環鏈表,循環鏈表裏尾結點不在是null,而是指向頭結點。當數據具備環形結構時候就可使用循環鏈表。

雙向循環鏈表

  與循環鏈表相似,尾結點指向頭結點,同時每一個結點除了保存自身數據,分別有一個前驅指針和後繼指針,就造成了雙向循環鏈表:

  

插入/刪除比較

在鏈表裏插入數據(new_node)

一、在P結點後插入數據

new_node->next = p->next;
p->next = new_node

此時時間複雜度爲O(1)

二、在P結點前插入數據

須要找到P結點的前驅結點,而後轉化爲在P的前驅結點以後插入數據。

  • 單鏈表須要從頭遍歷,當知足條件 pre->next = p時,轉化爲再pre結點後插入數據,此時時間複雜度爲O(n)遍;
  • 雙鏈表只須要經過p->pre便可找到前驅結點,時間複雜度爲O(1)

三、在值等於某個值的結點前/後插入數據

須要遍歷整個鏈表,找到這個值,而後在它前/後插入數據,此時時間複雜度爲O(n)

在鏈表裏刪除數據

一、刪除P結點下一個結點

p->next = p->next->next;

直接將p後繼結點指針指向p下下一個結點。

二、刪除P結點前一個結點

找到P結點前驅結點的前驅結點N,而後轉化爲刪除N的後繼結點

  • 單鏈表須要遍歷找到結點N,遍歷時間複雜度爲O(n),而後刪除N的一個後繼結點,時間複雜度爲O(1),因此總的時間複雜度爲O(n)
  • 雙向鏈表直接找到結點N:p->pre->pre->next = p,時間複雜度爲O(1)

三、在值等於某個值的結點前/後刪除數據

須要遍歷整個鏈表,找到這個值,而後在它前/後刪除數據,此時時間複雜度爲O(n)

棧是一種後進者先出,先進者後出的線性數據結構,只容許在一端插入/刪除數據。棧能夠用數組來實現,也能夠用鏈表來實現,用數組實現的叫順序棧,用鏈表來實現是叫鏈式棧。

棧的數組實現

數組來實現棧,插入和刪除都發生在數組的末尾,因此不須要進行數據的搬移,可是若是發生內存不夠須要進行擴容的時候,仍然須要進行數據搬移

 1     @Test
 2     public void testStack() {
 3         StringStack stack = new StringStack(10);
 4         for (int i = 0; i < 10; i++) {
 5             stack.push("hello" + i);
 6         }
 7         System.out.println(stack.push("dd"));
 8 
 9         String item = null;
10         while ((item = stack.pop()) != null) {
11             System.out.println(item);
12         }
13     }
14 
15     public class StringStack {
16         private String[] items;
17         private int count;
18         private int size;
19 
20         public StringStack(int n) {
21             this.items = new String[n];
22             this.count = 0;
23             this.size = n;
24         }
25 
26         public boolean push(String item) {
27             if (this.count == this.size) {
28                 return false;
29             }
30             this.items[count++] = item;
31             return true;
32         }
33 
34         public String pop() {
35             if (this.count == 0) {
36                 return null;
37             }
38             return this.items[--count];
39         }
40 
41         public int getCount() {
42             return count;
43         }
44 
45         public int getSize() {
46             return size;
47         }
48     }
View Code

棧的鏈表實現

鏈表的實現有個小技巧,倒着建立一個鏈表來模擬棧結構,最後添加到鏈表的元素做爲鏈表的頭結點,如圖:

 1 public class LinkStack {
 2     private Node top;
 3 
 4     public boolean push(String item) {
 5         Node node = new Node(item);
 6         if(top == null){
 7             top = node;
 8             return true;
 9         }
10         node.next = top;
11         top = node;
12         return true;
13     }
14 
15     public String pop() {
16         if (top == null) {
17             return null;
18         }
19         String name = top.getName();
20         top = top.next;
21         return name;
22     }
23 
24     private static class Node {
25         private String name;
26         private Node next;
27 
28         public Node(String name) {
29             this.name = name;
30             this.next = null;
31         }
32 
33         public String getName() {
34             return name;
35         }
36     }
37 }
View Code

數組實現的是一個固定大小的棧,當內存不夠的時候,能夠按照數組擴容方式實現棧的擴容,或是依賴於動態擴容的封裝結構來實現棧的動態擴容。出棧的時間複雜度都是O(1),入棧會有不一樣,若是是數組實現棧須要擴容,最好時間複雜度(不須要擴容的時候)是O(1),最壞時間複雜度是O(n),插入數據的時候,棧恰好滿了須要進行擴容,假設擴容爲原來的兩倍,此時時間複雜度是O(n),每n次時間複雜度爲O(1)夾雜着一次時間複雜度爲O(n)的擴容,那麼均攤時間複雜度就是O(1)。

棧的應用

  • 函數調用棧
  • java的攔截器
  • 表達式求解

隊列

隊列與棧相似,支持的操做也很類似,不過隊列是先進先出的線性數據結構。平常生活中經常須要進行的排隊就是隊列,排在前面的人優先。隊列支持兩個操做:入隊 enqueue 從隊尾添加一個元素;出隊 dequeue 從對列頭部取一個元素。

和棧同樣,隊列也有順序隊列和鏈式隊列分別對應數組實現的隊列和鏈表實現的隊列。

數組實現隊列

數組實現的隊列是固定大小的隊列,當隊列內存不足時候,統一搬移數據整理內存。

 1 public class ArrayQueue {
 2     private String[] items;
 3     private int capacity;
 4     private int head = 0;
 5     private int tail = 0;
 6 
 7     public ArrayQueue(int n) {
 8         this.items = new String[n];
 9         this.capacity = n;
10     }
11 
12     /**
13      * 入隊列(從隊列尾部入)
14      * @param item
15      * @return
16      */
17     public boolean enqueue(String item) {
18         //隊列滿了
19         if (this.tail == this.capacity) {
20             //對列頭部不在起始位置
21             if (this.head == 0) {
22                 return false;
23             }
24             //搬移數據
25             for (int i = head; i < tail; i++) {
26                 items[i - head] = items[i];
27             }
28             this.tail = tail - head;
29             this.head = 0;
30         }
31         items[tail++] = item;
32         return true;
33     }
34 
35     /**
36      * 出隊列(從隊列頭部出)
37      * @return
38      */
39     public String dequeue() {
40         if (this.head == this.tail) {
41             return null;
42         }
43 
44         return this.items[head++];
45     }
46 }
View Code

鏈表實現隊列

鏈表尾部入隊,從頭部出隊,如圖:

 1 public class LinkQueue {
 2     private Node head;
 3     private Node tail;
 4 
 5     public LinkQueue() { }
 6 
 7     /**
 8      * 入隊列
 9      * @param item
10      * @return
11      */
12     public boolean enqueue(String item) {
13         Node node = new Node(item);
14         //隊列爲空
15         if(this.tail == null) {
16             this.tail = node;
17             this.head = node;
18             return true;
19         }
20         this.tail.next = node;
21         this.tail = node;
22         return true;
23     }
24 
25     /**
26      * 出隊列
27      * @return
28      */
29     public String dequeue() {
30         //隊列爲空
31         if (this.head == null) {
32             return null;
33         }
34         String name = this.head.getName();
35         this.head = this.head.next;
36         if (this.head == null) {
37             this.tail = null;
38         }
39         return name;
40     }
41 
42     private static class Node {
43         private String name;
44         private Node next;
45 
46         public Node(String name) {
47             this.name = name;
48         }
49 
50         public String getName() {
51             return name;
52         }
53     }
54 }
View Code

循環隊列

數組實現隊列時,當隊列滿了,頭部出隊時候,會發生數據搬移,可是若是是一個首尾相連的環形結構,以下圖,頭部有空間,尾部到達7位置,再添加元素時候,tail到達環形的第一個位置(下標爲0)不須要搬移數據。

爲空的斷定條件:

head = tail

隊列滿了的斷定條件:

  • 當head = 0,tail = 7
  • 當head = 1,tail = 0
  • 當head = 4,tail = 3
  • head = (tail + 1)% 8
 1 public class CircleQueue {
 2     private String[] items;
 3     private int capacity;
 4     private int head = 0;
 5     private int tail = 0;
 6 
 7     public CircleQueue(int n) {
 8         this.items = new String[n];
 9         this.capacity = n;
10     }
11 
12     /**
13      * 入隊列(從隊列尾部入)
14      * @param item
15      * @return
16      */
17     public boolean enqueue(String item) {
18         //隊列滿了
19         if (this.head == (this.tail + 1)% this.capacity) {
20             return false;
21         }
22         items[tail] = item;
23         tail = (tail + 1) % this.capacity;
24         return true;
25     }
26 
27     /**
28      * 出隊列(從隊列頭部出)
29      * @return
30      */
31     public String dequeue() {
32         //隊列爲空
33         if (this.head == this.tail) {
34             return null;
35         }
36         String item = this.items[head];
37         head = (head + 1) % this.capacity;
38         return item;
39     }
40 }
View Code

 1 public class CircleQueue {
 2     private String[] items;
 3     private int capacity;
 4     private int head = 0;
 5     private int tail = 0;
 6 
 7     public CircleQueue(int n) {
 8         this.items = new String[n];
 9         this.capacity = n;
10     }
11 
12     /**
13      * 入隊列(從隊列尾部入)
14      * @param item
15      * @return
16      */
17     public boolean enqueue(String item) {
18         //隊列滿了
19         if (this.head == (this.tail + 1)% this.capacity) {
20             return false;
21         }
22         items[tail++] = item;
23         return true;
24     }
25 
26     /**
27      * 出隊列(從隊列頭部出)
28      * @return
29      */
30     public String dequeue() {
31         //隊列爲空
32         if (this.head == this.tail) {
33             return null;
34         }
35 
36         return this.items[head++];
37     }
3

鏈表實現循環隊列:

爲空的斷定條件:

head = tail

隊列滿了的斷定條件:

head = tail.next

 1 public class CircleLinkQueue {
 2     private Node head = null;
 3     private Node tail = null;
 4 
 5     public CircleLinkQueue(int n) {
 6         Node tempNode = null;
 7         // 初始化一個 n 大小的環形鏈表
 8         while (n >= 0) {
 9             Node node = new Node(String.valueOf(n));
10             if (this.head == null) {
11                 this.head = this.tail = node;
12                 this.head.next = this.head;
13                 tempNode = this.head;
14             } else {
15                 tempNode.next = node;
16                 tempNode = tempNode.next;
17                 tempNode.next = this.head;
18             }
19             n--;
20         }
21     }
22 
23     /**
24      * 入隊列(從隊列尾部入)
25      *
26      * @param item
27      * @return
28      */
29     public boolean enqueue(String item) {
30         //隊列滿了
31         if (this.head == this.tail.next) {
32             return false;
33         }
34         this.tail.setValue(item);
35         this.tail = this.tail.next;
36         return true;
37     }
38 
39     /**
40      * 出隊列(從隊列頭部出)
41      *
42      * @return
43      */
44     public String dequeue() {
45         //隊列爲空
46         if (this.head == this.tail) {
47             return null;
48         }
49         String item = this.head.getValue();
50         this.head = this.head.next;
51         return item;
52     }
53 
54     public Node peek() {
55         //空隊列
56         if (this.head == this.tail) {
57             return null;
58         }
59         return this.head;
60     }
61 
62     private class Node {
63         private String value;
64         private Node next;
65 
66         public Node(String value) {
67             this.value = value;
68         }
69 
70         public String getValue() {
71             return value;
72         }
73         public void setValue(String value) {
74             this.value = value;
75         }
76     }
77 }
View Code

阻塞隊列

阻塞隊列是指當頭部沒有元素的時候(對應隊列爲空),出隊會阻塞直到有元素爲止;或者隊列滿了,尾部不能再插入數據,直到有空閒位置了再插入。

併發隊列

線程安全的隊列叫併發隊列。dequeue和enqueue加鎖或者使用CAS實現高效的併發。

附錄

本文demo

 

後續


首先感謝@李勇888提供建議。而後抽空實現了一下,加一個size來表示循環隊列裏元素的數量,實現demo參考:ArrayCircleQueue 

相關文章
相關標籤/搜索