文章目錄
定義
最簡單的最常用的數據結構,以排隊的方式去組織數據
線性,就是像線一樣,前面是誰,後面是誰,串聯好的次序
一對一:每個數據前面只能有一個數據,後面也只能有一個數據。前面沒數據的就是起點,後面沒數據的就是終點。
官方概念:線性表是0個或多個數據元素的有限序列。(0個元素則叫做空表)
線性表的一個數據元素可以有多個數據項。
操作
這裏只寫了一些最基本的使用最普遍的,實際上不同的應用中,需要線性表的操作可能多種多樣,但是一般都可以用下面這些基本操作的組合來實現。
比如A和B兩個線性表的並就是把A和B兩個表的元素都插入到一個新的線性表中,但是重複項只插入一次
- 創建,初始化:建立一個空的線性表
- 重置爲空表
- 查找:根據數據的位序找到數據元素,就像數組根據索引找值
- 查找某個元素是否存在
- 獲得線性表的長度
- 插入一個數據
- 刪除一個數據
//操作,沒寫返回值,其中參數爲指針的都可以改爲引用或者按值傳遞
InitList(*L);//參數是指向List的指針
IsListEmpty(*L);//如果爲空,返回true
ClearList(*L);//清空重置
GetElem(*L, index, *e);//把線性表中index處的內容給e指向的位置
LocateElem(*L, e);//查找線性表中有無元素e,如果有,返回位置(索引),否則返回0
ListInsert(*L, index, e);//在index處插入元素e
ListDelete(*L, index, *e);//刪除線性表的index處的元素,放入e指向位置
ListLength(*L);//返回元素個數,即線性表長度
假設把線性表A和B的union放入A:
void ListUnion(*La, *Lb)
{
unsigned int len_a = ListLength(*La);
unsigned int len_b = ListLength(*Lb);
ElemType e;
int i;
for (i = 0; i < len_b; ++i)
{
GetElem(*Lb, i, *e);//這裏只是僞代碼,注意實際中不能這樣,沒初始化就解引用
if (!LocateElem(*La, e))
{
ListInsert(*La, ++len_a, e);
}
}
}
兩種物理結構
順序存儲結構
關鍵詞:地址連續,每個數據元素的數據類型都一樣
用一維數組實現順序存儲結構的線性表
描述順序存儲結構需要三個屬性:
- 起始位置
- 線性表最大存儲容量
- 線性表當前長度(小於等於最大長度)
可以用C語言實現,或者其他語言:
typedef int Type;//增加代碼的通用性
const int MAXSIZE = 10;//最大容量
typedef struct
{
Type data[MAXSIZE];//data數組名,就是起始位置
int length;//當前長度
}SqList;
隨機存取結構:存取時間性能是 0(1)的存儲結構
由於一維數組這種線性表的起始位置知道,地址又連續,所以其中任意位置的地址可以被很簡單地立即算出,用時間複雜度的概念來說就是:存取時間性能是O(1)。
這種存儲結構我們就成爲隨機存儲結構。
所以順序結構的線性表是一種隨機存儲結構。
獲得元素操作(非常簡單)
對於順序結構的線性表,返回某個位置的元素非常簡單,這就是隨機訪問,因爲順序存儲線性表就是隨機存儲結構。
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
Status GetElem(SqList L, int i, ElemType *e)
{
if (L.length == 0 || i < 0 || i > L.length)
return ERROR;
*e = L.data[i];
return OK;
}
插入操作(複雜)
Status ListInsert(SqList * L, int i, ElemType e)
{
int k;
if (L->length == MAXSIZE)
return ERROR;
if (i < 0 || i > L->length)
return ERROR;
if (i < L->length - 1)//插入位置不在表尾
{
for (k = L->length-2;k>=i;k--)
L->data[k+1] = L->data[k];
}
L->data[i] = e;//插入新元素
L->length++;
return OK;
}
複雜度:
- 最好情況:插入位置是線性表的末尾,不需要移動元素,所以複雜度O(1)
- 最壞情況:插入位置是線性表的起始位置,即第一個位置,需要所有元素後移一步,所以複雜度O(n),n是線性表長度
- 平均情況:插入到第i個位置,需要移動n-i個元素,如果i靠前,則移動的元素就多,反之反之。平均起來,i取n/2,則複雜度,忽略常數,即仍然是O(n).
所以,線性表的順序存儲結構在存入或者讀取數據時的複雜度是O(1);在插入或者刪除數據的複雜度是O(n).所以他很適合不經常插入和刪除,而是經常讀取和存入(不是插入)的應用場景。
刪除操作
- 隊列長度如果爲0,拋出異常
- 再看索引範圍是否正確,刪除位置不合理則拋出異常
- 取出刪除元素
- 從索引i+1開始,一直到length-1的元素全部往前移動一位
- 長度減1
Status DeleteList(SqList * L, int i, ElemType & e)
{
int k;
if (L->length == 0)
return ERROR;
if (i < 0 || i > L->length-1)
return ERROR;
e = L->data[i];//取出元素,這裏由於要保證函數執行完畢後e的值不丟失,使用引用類型
for (k = i + 1; k < length; ++k)
{
L->data[k-1] = L->data[k];
}
L->length--;
return OK;
}
刪除操作的算法複雜度和插入一毛一樣。
優缺點
優點第一點:比如鏈表就需要除了數據項以外,還要給一個指針分配空間,用於指向下一個數據項的位置。
鏈式存儲結構:“亂”中有“序”
如圖,物理內存位置亂七八糟,不像順序結構那樣緊緊挨着排成一列,非常集中,可以相鄰也可以不相鄰,是一種動態的零散的結構;但是邏輯位置卻又十分有序,是一個線性的單鏈。我覺得亂中有序來形容鏈表,雙重意味,最貼切不過。
但是鏈式結構需要額外空間存儲下一個元素的地址,不僅要處處數據項。存數據的域叫做數據域,存後繼元素地址的域叫做指針域。這兩部分組合在一起,成爲一個數據元素的存儲映像,被稱爲結點Node.
單鏈表:結點只有一個指針域
如果每個結點只有一個指針域,就叫做單鏈表。因爲這唯一的一個指針域中存儲的是指向後繼結點的指針next,只能訪問後繼結點,所以是單向的。
頭指針:指向第一個元素,不可能爲空指針
第一個結點的內存位置叫做頭指針,即指向第一個元素
就算鏈表是空的,頭指針也不可能是空的,因爲頭指針一定會指向第一個結點,如果是空鏈表,也會有第一個結點,只不過第一個結點的數據域不存東西,指針域存空指針(這就表示這個結點也是最後一個結點)。
他是鏈表的必需元素,不能沒有
但是如果鏈表有頭結點,則頭指針是頭結點指針域中存儲的那個指針
尾指針:NULL,空指針
頭結點(可以沒有但一般都會用,因爲它可以讓空鏈表和非空鏈表的處理一致)
有時候會專門給鏈表附加一個頭結點,不是爲了存儲數據,只是爲了操作方便和統一設立的,數據域可以不存東西,也可以存整個鏈表的長度信息。
- 方便: 可以直接獲取到鏈表長度,無需遍歷一次
- 統一:有了頭結點,則對第一個元素前面插入元素,以及刪除第一個元素的操作,就和其他節點一致了;且頭結點使得所有鏈表(即使是空鏈表)的頭指針都不爲空指針(空鏈表的頭結點的指針域中的指針爲空指針)。這種統一性是頭結點帶來的最大的好處。
當有頭結點的時候,頭指針指向頭結點,頭結點指針域的指針指向第一個常規結點;
沒有頭結點的時候,頭指針指向第一個常規結點(存儲數據)。
頭指針並不是頭結點的指針域中的內容!
- 沒有頭結點的單鏈表(頭指針指向第一個常規結點,尾結點指針域的符號表示空指針)
- 有頭結點的單鏈表(頭指針指向頭結點,頭結點指針域的指針指向第一個常規結點)
頭結點的數據域是黑色代表沒有數據項,但是可以存其他附加信息
- 有頭結點的空鏈表(頭指針指向頭結點,頭結點指針域的指針指向第一個常規結點)
這時候頭結點的指針域存儲空指針
所以頭結點的好處是:使得空鏈表中頭指針也不爲空指針!!空鏈表的頭結點的指針域中的指針爲空指針
用C的結構實現結點
typedef struct
{
ElemType data;//數據域
Struct Node * next;//指針域
}Node, *LinkList;
//typedef struct Node * LinkList;
如果p是一個指針,且p->data = ,則p->next->data=
獲取元素操作:遍歷,指針右移
只能遍歷,一個一個找。核心就是利用指針的右移一個一個地遍歷,其實很多算法都要用到這種技術。
Status GetElem(LinkList L, int i, ElemType & e)
{
LinkList p = L->next;//L是頭指針,指向頭結點,p被初始化爲第一個結點的地址,即指向第一個結點
int j = 0;
while (p && j < i)
{
p = p->next;
++j;
}
if (j == i)
{
e = p->data;
return OK;
}
return ERROR;//無此數據
}
算法時間複雜度取決於i的位置:
最好情況,i=0,則立刻就找到了,不需要遍歷
最壞情況,i=n-1,n爲鏈表長度,則需要遍歷n-1次,最壞情況時間複雜度是O(n)
插入和刪除
都是先遍歷找到索引爲i的元素,然後插入或者刪除。時間複雜度也和i的位置有關,i=0是最好情況,無需遍歷,最壞情況i=n-1,光遍歷的時間複雜度就是O(n),但是插入和刪除的核心操作都只是一句代碼的事,時間複雜度是O(1)。
所以插入和刪除的時間複雜度總的來說都是O(n),主要時間都花在遍歷上了。
插入操作
s->next = p->next;
p->next = s;
Status InsertItem(LinkList L, int i, ElemType e)
{
LinkList s;
LinkList p = L->next;//L指向頭結點,p指向第一個結點
int j = 0;
while (p && j < i)
{
p = p->next;
++j;
}
if (!p)
return ERROR;
LinkList s = new LinkList;//C++可以在任何位置聲明變量
//C形式,要在函數最前面聲明s
//s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;//p現在指向第i-1個元素
p->next = s;
return OK;
}
刪除操作
Status DeleteItem(LinkList L, int i, ElemType * e)
{
//L是頭結點中存的指針,指向第一個常規結點
LinkList p = L->next;
int j = 0;
while (p->next && j < i)
{
p = p->next;
++j;
}
if (!(p->next))
return ERROR;
LinkList q = p->next;//必須用q存住p->next以釋放它
*e = p->next->data;
p->next = p->next->next;//核心代碼就這一句
free(q);
return OK;
}
單鏈表的動態創建
順序結構的線性表的創建,實際上就是初始化一個數組,只需要告訴數組元素的數據類型和數組長度;
但是鏈表是動態結構,事先不知道鏈表長度,所以不能事先分配,只能根據需求即時生成一個結點並插入到鏈表中。
頭插法:把新結點插在頭結點後面,做第一個常規結點
每次把新結點插在頭結點後面。做第一個常規結點。
void CreateListHead(LinkList *L, int n)
{
//L是指向頭指針(*L)的指針,因爲LinkList 是struct Node *
//n是要創建的項數
LinkList p;
int i;
srand(time(0));//設置隨機數種子
//先創建空鏈表:只有頭結點,且頭結點的指針域中存空指針
*L = (LinkList)malloc(sizeof(Node));//*L是頭指針,指向剛分配的結點:頭結點
(*L)->next = NULL;//空鏈表創建成功
for (i = 0; i < n; ++i)
{
//生成新節點
p = (LinkList)malloc(sizeof(Node));
p->data = rand() %100 + 1;
//把新節點p插入到第一個位置
p->next = (*L)->next;
(*L)->next = p;
}
}
徹底被這幾個指針搞暈了,拿了紙筆一通分析,得到以下結論:
-
L
是指向頭指針的指針,頭指針指向頭結點,頭結點的指針域中存的指針指向第一個常規結點!,L
是LinkList *
類型 -
*L
是頭指針,指向頭結點,而頭結點的指針域中存的那個指針指向第一個結點(非頭結點,第一個存內容的常規結點)。*L
,解引用一下,是LinkList
類型,即struct Node *
類型。 -
**L
是頭結點。對*L
再解引用一下,得到Node
類型。
我畫了個草圖:
尾插法
每次把新結點插在前一個新結點後面,即終端節點的後面,先來後到。
這裏要用一個指向尾結點的指針rear
void CreateListTail(LinkList * L, int n)
{
LinkList p, r;
int i;
srand(time(0));
//先創建空鏈表
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
r = *L;//指向尾結點
//循環創建新結點,並隨機給數據域賦值
for (i = 0; i < n; ++i)
{
p = (LinkList)malloc(sizeof(Node));//新結點
p->data = rand()%100 + 1;
r->next = p;
r = p;
}
r->next = NULL;
}
單鏈表的整表刪除:在內存中釋放掉它佔用的空間
由於鏈表的每一個結點都是動態創建的,是動態分配地堆內存,所以刪除鏈表必須要釋放每一個結點的地址,否則就會有內存泄漏的危險。
考慮挨個釋放,先把第一個free,問題來了,第二個結點在哪兒都不知道了······因爲第二個結點的位置在第一個結點裏。所以:
- 聲明新結點p和q
- 把第一個結點賦給p
- 循環:
- 把下一結點給q
- 釋放p
- 把q賦給p
Status ClearList(LinkList *L)
{
//*L是頭指針
LinkList p, q;
p = (*L)->next;//p指向第一個結點
while (p)
{
q = p->next;
free(p);
p = q;
}
//至此p爲空指針
(*L)->next = NULL;//頭結點的指針域存儲空指針,表示空鏈表
return OK;
}
順序結構和單鏈表結構的對比
說白了就是單鏈表更靈活。
順序結構的唯一優點:查找快。如果非要再加一個,那就是代碼不好寫點。
單鏈表的唯一缺點:查找慢。如果非要再加一個,那就是代碼好寫點。
這麼看的話,我還是乖乖用鏈表吧。
當然,我上面兩句總結有些偏頗,不能說鏈表就一定比順序結構的數組好,要看應用場景,有時候還真是數組合適一些。