一.寫在前面
“生活不止眼前的苟且,還有詩和遠方的田野,你赤手空拳的來到人世間,爲找到那篇海不顧一切”,高曉松說。我們學習開發這麼多年,也明白“開發不止當下的bug,還有將來和未發現的bug,我們在開發的路上不斷探索,只爲找尋那優質的產品”。開發猶練功,可分爲外功招式和內功心法,自計算機問世以來,曾經出現了好多外功招式(編程語言)以及內功心法(數據結構)。各大外功招式都曾名噪一時,有被時間遺忘的,有曾曇花一現的,也有經久不衰的。語言總體分爲彙編語言,機器語言,腳本語言和高級語言。而我們所常見的語言有C、C++、C#、Java、python、php、易語言等等,不管是像C和Java這類編譯型語言還是像python這類解釋型語言,他們都有一個相通的東西,那就是編程思想。語言只是工具,思想纔是精髓。而在編程興起的長河中,有一套基本適用於大部分語言的內功心法——數據結構。本篇文章以線性表(List)開始說起。
二.基本概念
-
基本定義
線性表(List):零個或多個數據元素的有限序列。
-
關鍵點
1.首先線性表是一個序列:元素之間有確定的順序;
2.線性表是有限的:元素的個數n爲線性表的長度,當n爲0時,記爲空表。
三. 線性表的存儲結構
順序存儲結構
1. 基本知識
-
用一段地址連續的存儲單元依次存儲線性表的數據元素
線性表(a1,a2,……an)的順序存儲示意圖如下:
順序存儲結構簡單說就是在內存中開闢了一塊內存,按照依次佔位的方式存入元素。
2.插入與刪除
獲取操作:線性存儲結構獲取元素非常簡單,我們只需要傳入我們想獲取的第幾個元素,線性表就會根據下標找到這個元 素。
插入操作 :
插入操作的思路如下:
- 如果插入位置不合理,則拋出異常;
- 如果線性表長度大於等於數組長度,則拋出異常或者動態增加容量;
- 從最後一個元素開始向遍歷到第i個位置,分別對它們都向後移動一個位置;
- 將要插入的元素填入到第i個位置;
- 表長加1。
刪除操作:
刪除操作的思路如下:
- 如果刪除位置不合理,則拋出異常;
- 取出刪除元素;
- 從刪除元素位置開始遍歷到最後一個元素位置,分別將它們向前移動一個位置;
- 表長減1。
線性表的順序存儲結構,在存、讀數據元素時,它的時間複雜度爲O(1),插入和山刪除時,時間複雜讀度爲O(n)。因此這種存儲方式適合元素個數不太變換,更多的操作是存讀數據的應用。
3.優缺點
優點 | 缺點 |
---|---|
|
|
-
線性表的鏈式存儲結構
前面已經提到,在線性表的順序存儲結構中,當對錶中元素進行插入與刪除操作時需要改動大量的
元素來完成操作,因而容易耗費大量的時間。因此我們追尋一種能提升工作效率的方法。
1.基本知識
在線性表的鏈式存儲結構中,每個數據元素ai與其直接後繼數據元素ai+1之間的邏輯關係除了存儲其本身的信息之外,還需要存存儲一個指示其直接後繼的信息(即直接後繼的存儲位置)。我們把存儲數據元素信息的域稱爲數據域,把存儲直接後繼位置的域稱爲指針域。指針域中存儲的信息稱爲指針或鏈。這兩部分信息組成數據元素ai的存映射,稱爲結點(Node)。
n個結點鏈成一個鏈表,即爲線性表的鏈式存儲結構,因爲此鏈表的每個結點只包含一個指針域,所以叫單鏈表。如下圖所示:
我們把鏈表中第一個結點的存儲位置叫做頭指針。
頭指針與頭結點的區別:
頭指針 | 頭結點 |
|
|
2.獲取、插入與刪除
在線性鏈表的存儲結構中,我們要計算任何一個元素是很容易的,但在鏈式存儲方式中,無法知道第i個元素在哪裏,必須從頭開始查找。
獲取第i個元素的思路:
- 聲明一個節點p指向鏈表的第一個節點,初始化j從開始;
- 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一節點,j累加1;
- 若到鏈表末尾p爲空,則說明第i個元素不存在;
- 否則查找成功,返回節點p的數據。
在進行單鏈表的插入時,假設存儲元素e的結點爲s,要實現結點p、p->next和s之間邏輯關係的變化,只需要
將結點s插入到結點p和p->next之間即可。其他結點無需更改。
單鏈表第i個數據插入結點的算法思路:
- 聲明一結點p指向鏈表的第一個結點,初始化j從1開始
- 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1
- 若到鏈表末尾p爲空,則說明第i個元素不存在
- 否則查找成功,在系統中生成一個空結點s
- 將數據元素e賦值給s->data
- 單鏈表的插入標準語句爲s->next=p->next;p->next = s
- 返回成功
在進行單鏈表刪除時,設存儲元素ai的結點爲q,要實現將結點q刪除操作,其實就是將它的前繼結點的指針繞過,指向它的後繼結點即可。如下圖:
單鏈表第i個數據刪除思路:
- 聲明結點p指向鏈表第一個結點,初始化j從1開始
- 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一節點,j累加1
- 若到鏈表末尾p爲空,則說明第i個元素不存在
- 否則查找成功,將欲刪除的結點p->next賦值給q
- 單鏈表的刪除標準語句p->next=q->next
- 將q結點中的數據賦值給e,作爲返回
- 釋放q結點
- 返回成功
我們發現單鏈表的插入和刪除操作,它們都有兩部分組成:第一部分是遍歷查找第i個元素;第二部分是插入和刪除元素。它們的時間複雜度都是O(n)。很顯然對於插入或者刪除數據比較頻繁的操作時,單鏈表的效率明顯高於順序方式存儲。
-
單鏈表結構與順序存儲結構比較
單鏈表結構與順序存儲結構比較
存儲分配方式 時間性能 空間性能 查找 插入和刪除 順序存儲結構 連續的一段存儲單元依次存儲 O(1) 需要平均移動表長一半的元素,時間爲O(n) 需要預分配存儲空間,分大了,浪費,分小了,溢出 鏈式存儲結構 一組任意的存儲單元存放 O(n) 在找出某位置指針後,時間爲O(1) 不需要預分配,只要有就分配,元素個數不受限
在C/C++語言陣營裏因爲具有指針的能力,可以很方便的進行鏈表的操作,而在java,C#陣營裏,雖然不使用指針,但因爲啓用了引用機制,從某種角度也間接實現了指針的一些作用,而像早起的Basic,Fortran由於沒有指針,它們用數組來代替,通常我們把這種用數組描述的鏈表叫做靜態鏈表。
-
循環鏈表
對於單鏈表而言,由於每個結點只存儲了向後的指針,到了尾標誌就停止了。如果我們將單鏈表的末尾結點的指針端由空指針改爲指向頭結點,那麼就使整個單鏈表形成一個環,生生不息,無盡無窮,這種頭尾相連的單鏈表稱爲單循環鏈表,簡稱循環鏈表(circular linked list)。如圖:
-
雙向鏈表
鏈表中的結點只包含一個指向後繼的指針我們稱爲單鏈表,當我們在設置一個指向前驅結點的指針域,它就成了雙向鏈表。所以在雙向鏈表中的結點都有兩個指針域,一個指向直接後繼,另一個指向直接前驅。如圖:
三.實例代碼
話不多說直接上代碼:
#include<stdlib.h> #include <iostream> using namespace std; //首先定義一個結點 struct Node { int data; Node* next; }; //定義一個鏈表類 class ListLink { public: ListLink(); ~ListLink(); public: bool clearList(); inline bool isEmpty() { return m_head == NULL; } int Length(); bool GetElem(const int index, int *data); //獲取元素 bool Insert(const int index, int data); //插入元素 bool Delete(const int index, int *data); //刪除元素 private: Node* m_head; }; ListLink::ListLink() :m_head(NULL) { } ListLink::~ListLink() { Node *p = m_head; while (m_head) { p = m_head; m_head = m_head->next; delete(p); } } bool ListLink::clearList() { Node *p = m_head; while (m_head) { p = m_head; m_head = m_head->next; delete(p); } return true; } int ListLink::Length() { Node *p = m_head; int len = 0; while (p != NULL) { len++; p = p->next; } return len; } bool ListLink::GetElem(const int index, int *data) { Node *p = m_head; int j = 0; while (p&&j < index) { p = p->next; j++; } if (p == nullptr) { return false; } *data = p->data; return true; } bool ListLink::Insert(const int index, int data) { Node *p = m_head; Node *s; int j = 0; if (index == 0) { s = (Node *)new Node[1]; s->data = data; s->next = p; m_head = s; return true; } while (p&&j < index - 1) { p = p->next; j++; } if (p == NULL) { return false;//到隊尾了 } s= (Node *)new Node[1]; s->data = data; s->next = p->next; p->next = s; return true; } bool ListLink::Delete(const int index, int *data) { Node *p = m_head; Node *s; if (p == NULL) { return false; } int j = 0; if (index == 0) { m_head = m_head->next; *data = p->data; delete p; p = NULL; return true; } while (p&&j < index - 1) { j++; p = p->next; } if (p == NULL) return false; s = p->next; p->next = p->next->next; *data = s->data; delete s; s = NULL; return true; } //主函數測試 int _tmain(int argc, _TCHAR* argv[]) { int a = 0; int *p = &a; ListLink list; //插入測試 list.Insert(0, 1); list.Insert(1, 2); list.Insert(2, 3); list.Insert(3, 4); list.Insert(3, 5); list.Insert(1, 6); //鏈表長度 cout <<"鏈表長度:="<< list.Length()<<endl; //插入的元素值 cout << "各個元素的值依次是:"<< endl; for (int i = 0;i < list.Length();i++)//遍歷該鏈表 { if (list.GetElem(i, p)) { cout << *p<<endl; } } cout << endl; //刪除元素 int e = 3; list.Delete(2,&e); //鏈表長度 cout <<"刪除元素後鏈表長度:="<< list.Length()<<endl; //刪除元素之後剩下的元素 cout << "刪除元素之後剩下各個元素的值依次是:"<< endl; for (int i = 0;i < list.Length();i++)//遍歷該鏈表 { if (list.GetElem(i, p)) { cout << *p<<endl; } } cout << endl; //清空鏈表 list.clearList(); //鏈表長度 cout <<"清空後鏈表長度:="<< list.Length()<<endl; system("pause"); }
程序輸出測試結果如下:
}
【下一篇:】數據結構淺淺析之(二)——棧和隊列(Stack && Queue):https://blog.csdn.net/weixin_39951988/article/details/86518425