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


線性表

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

數組

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

內存分布:

隨機訪問

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

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

其中:

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

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

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

所以訪問數組里指定下標的任何一個元素,都可以直接訪問對應的地址,不需要遍歷數組,時間復雜度為O(1)。

插入/刪除低效

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

向數組里插入一個元素有三種情況:

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

2、向數組開頭插入一個元素,需要將所有元素依次向后挪一個位置,然后將元素插入開頭位置,此時時間復雜度為O(n),對應最壞時間復雜度。

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

如果數組元素是沒有順序的(或不需要保證元素順序),向數組中間位置插入一個元素x,只需要將插入位置的元素放到數組的末尾,然后將x插入,這時候不需要搬移元素,時間復雜度仍為O(1),如:

 

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

如果不需要保證數據的連續性,有兩種方法:

1、可以將末尾的數據搬移到刪除點插入,刪除末尾那個元素

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

Java的ArrayList

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

什么時候選擇使用編程語言幫我們封裝的數組,什么時候直接使用數組呢?

1、Java ArrayList不支持基本數據類型,需要封裝為Integer、Long類才能使用。Autoboxing、Unboxing有一定的性能消耗。如果比較關注性能可以直接使用數組

2、使用前已經能確認數據大小,並且操作比較簡單可以使用數組

鏈表

  比起數組,鏈表不需要連續內存空間,它通過指針將一組分別獨立的內存空間串起來形成一條“鏈條”。鏈表有很多種,如:單鏈表、雙向鏈表、循環鏈表和雙向循環鏈表。

  鏈表內存分布:

單鏈表

如下圖所示,鏈表的每一項我們稱為結點(node),為了將所有結點連接起來形成鏈表,結點除了需要記錄數據之外,還需要記錄下一個結點的地址,記錄下一個結點地址的指針我們叫做后繼指針(next),第一個結點和最后一個結點比較特殊,第一個結點沒有next指針指向它,稱為頭結點,最后一個結點next指針沒有指向任何元素,稱為尾結點。頭結點用來記錄鏈表的基地址,有了它就可以順着鏈子往下搜索每一個元素,如果遇到next指向null表示到達了鏈表的末尾。

在數組里插入或刪除數據需要保證內存空間的連續性,需要做數據的搬移,但是鏈表里數據內存空間是獨立的,插入刪除只需要改變指針指向即可,所以鏈表的插入刪除非常高效。如圖:

雙向鏈表

  單向鏈表每個結點只知道自己下一個結點是誰,是一條單向的“鏈條”,而在雙向鏈表里每個結點既知道下一個結點,還知道前一個結點,相比單鏈表,雙向鏈表每個結點多了一個前驅指針(prev)指向前一個結點的地址,如下圖所示:

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

循環鏈表

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

雙向循環鏈表

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

  

插入/刪除比較

在鏈表里插入數據(new_node)

1、在P結點后插入數據

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

此時時間復雜度為O(1)

2、在P結點前插入數據

需要找到P結點的前驅結點,然后轉化為在P的前驅結點之后插入數據。

  • 單鏈表需要從頭遍歷,當滿足條件 pre->next = p時,轉化為再pre結點后插入數據,此時時間復雜度為O(n)遍;
  • 雙鏈表只需要通過p->pre即可找到前驅結點,時間復雜度為O(1)

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

需要遍歷整個鏈表,找到這個值,然后在它前/后插入數據,此時時間復雜度為O(n)

在鏈表里刪除數據

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

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

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

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

找到P結點前驅結點的前驅結點N,然后轉化為刪除N的后繼結點

  • 單鏈表需要遍歷找到結點N,遍歷時間復雜度為O(n),然后刪除N的一個后繼結點,時間復雜度為O(1),所以總的時間復雜度為O(n)
  • 雙向鏈表直接找到結點N:p->pre->pre->next = p,時間復雜度為O(1)

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

需要遍歷整個鏈表,找到這個值,然后在它前/后刪除數據,此時時間復雜度為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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM