1. 線性表的順序存儲結構
用一段連續的存儲單元一次存儲線性表的數據元素。
結構代碼:
const int MAXSIZE = 20; // 存儲空間初始分配量
typedef int ElemType; // ElemType類型根據實際情況而定,這裏假設爲int
typedef struct
{
ElemType data[MAXSIZE]; // 數組存儲數據元素
int length; // 線性表當前長度
}SqList;
這裏,我們發現描述順序存儲結構需要三個屬性:
- 存儲空間的起始位置:數組data。
- 線性表的最大存儲容量:數組長度MAXSIZE。
- 線性表的長度:length。
1.1 數組長度與線性表長度的區別:
數組長度是存放線性表存儲空間的長度,線性表長度是線性表中數據元素的個數。任意時刻線性表的長度應小於等於數組長度。
其存取性能爲O(1),通常把具有這一特點的存儲結構成爲隨機存儲結構。
1.2 順序存儲結構的插入與刪除
1.2.1 插入操作:
// 初始條件:順序線性表L已經存在,1<=i<=ListLength(L)
// 操作結果:在L中第i個位置之前插入新的數據元素e,L長度加1
Status ListInsert(SqList *L, int i, ElemType e)
{
int k;
if (L->length == MAXSIZE) // 順序線性表已滿
return ERROR;
if (i<1 || i>L->length+1) // 當i不在範圍內時,注意是length+1
return ERROR;
if (i<=L->length) // 當插入數據位置不在表尾
{
for (k=L->length-1; k>=i-1; k--) // 將要插入位置後數據元素後移一位
{
L->data[k+1] = L->data[k];
}
}
L->data[i-1] = e;
L->length++;
return Ok;
}
1.2.2 刪除操作:
// 初始條件:順序線性表L已經存在,1<=i<=ListLength(L)
// 操作結果:刪除在L中第i個位置之前的數據元素e,L長度減1
Status ListDelete(SqList *L, int i, ElemType *e)
{
int k;
if (L->length == 0) // 順序線性表爲空
return ERROR;
if (i<1 || i>L->length) // 當i不在範圍內
return ERROR;
if (i<L->length) // 當刪除位置不在表尾
{
for (k=i-1; k<L->length; k++) // 將要刪除位置後數據元素前移一位
{
L->data[k] = L->data[k+1];
}
}
L->length--;
return Ok;
}
複雜度
最好情況O(1),最差情況O(n),平均複雜度爲O((n-1)/2) = O(n)。
這說明,它比較適合於元素個數不太變化,而更多是存取數據的應用。
1.3 線性表順序存儲結構的優缺點:
優點:
- 無需爲表中元素之間的邏輯關係而增加額外的存儲空間
- 可以快速存取表中任意位置的元素
- 插入刪除操作需要移動大量元素
- 當線性表長度變化較大時,難以確定存儲空間容量
- 造成存儲空間“碎片”
2. 線性表的鏈式存儲結構
鏈表中第一個結點的存儲位置叫做頭指針。線性鏈表的最後一個結點指針爲“空”(通常用NULL或“^”符號表示)。
有時,我們爲了更加方便的對鏈表進行操作,會在單鏈表的第一個結點前附設一個結點,稱爲頭結點。頭結點的數據域可以不存儲任何信息,也可以存儲如線性表長度等附加信息,頭結點的指針域存儲指向第一個結點的指針。
頭結點與頭指針的異同:
頭指針:
- 頭指針是指鏈表指向第一個結點的指針,若鏈表有頭結點,則是指向頭結點的指針
- 頭指針具有標識作用,所以常用頭指針冠以鏈表的名字
- 無論鏈表是否爲空,頭指針均不爲空。頭指針是鏈表的必要元素
- 頭結點是爲了操作的統一和方便而設立的,放在第一個元素的結點之前,其數據域一般沒有意義(也可以放鏈表長度)
- 有了頭結點,對在第一個結點前插入和刪除第一個結點,其操作與其他結點的操作就統一了
- 頭結點不一定是鏈表的必要元素
// 線性表的單鏈表存儲結構
typedef struct Node
{
ElemType data;
struct Node *next;
}Node;
typedef struct Node *LinkList; // 定義LinkList
2.1 單鏈表的讀取
// 初始條件:順序線性表L已經存在,1<=i<=ListLength(L)
// 操作結果:用e返回L中第i個數據元素的值
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p;
p = L->next; // 讓p指向鏈表L的第一個結點
j = 1;
while(p && j<i)
{
p = p->next;
++j;
}
if (!p || j>i)
{
return ERROR; // 第i個元素不存在
}
*e = p->data; // d取第i個元素的數據
return Ok;
}
說白了,就是從頭開始找,直到第i個元素爲止。時間複雜度取決於i的位置,i=1時不需要遍歷,而i=n時,則遍歷n-1次纔可以。
2.2 單鏈表的插入與刪除
2.2.1 單鏈表的插入
// 初始條件:順序線性表L已經存在,1<=i<=ListLength(L)
// 操作結果:在L中第i個位置之前插入新的數據元素e,L長度加1
Status ListInsert(LinkList *L, int i, ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while(p && j<i)
{
p = p->next;
++j;
}
if (!p || j>i)
{
return ERROR; // 第i個元素不存在
}
s = (LinkList)malloc(sizeof(Node));// 生成新結點
s->data = e;
s->next = p->next;// 將p的後繼結點賦值給s的後繼
p->next = s; // 將s賦值給p的後繼
return Ok;
}
對於單鏈表的表頭和表尾的特殊情況,操作是相同的。2.2.2 單鏈表的刪除
// 初始條件:順序線性表L已經存在,1<=i<=ListLength(L)
// 操作結果:刪除在L中第i個位置之前的數據元素e,L長度減1
Status ListDelete(LinkList *L, int i, ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next && j<i)
{
p = p->next;
++j;
}
if (!(p->next) || j>i)
{
return ERROR; // 第i個元素不存在
}
q = p->next;
p->next = q->next;
*e = q->data; // 將q結點中的數據給e
free(q); // 回收此結點
return Ok;
}
對插入刪除數據越頻繁的操作,單鏈表的效率優勢就越明顯。
2.3 單鏈表的整表創建
創建單鏈表的過程就是一個動態生成鏈表的過程,即從“空表”的初始狀態起,依次建立各個元素結點,並逐個插入鏈表。
2.3.1 頭插法
// 隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(頭插法)
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); // 初始化隨機數種子
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; // 先建立一個帶頭結點的單鏈表
for (i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); // 生成新結點
p->data = rand()%100+1;// 隨機生成100內數字
p->next = (*L)->next;
(*L)->next = p;// 插入到表頭
}
}
2.3.2 尾插法
// 隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(尾插法)
void CreateListTail(LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); // 初始化隨機數種子
*L = (LinkList)malloc(sizeof(Node));
r = *L; // r爲指向尾部的結點
for (i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); // 生成新結點
p->data = rand()%100+1;// 隨機生成100內數字
r->next = p;// 將表尾結點的指針指向新結點
r = p;// 將當前的新結點定義爲表尾結點
}
r->next = NULL;
}
2.4 單鏈表的整表刪除
// 初始條件:順序線性表L已經存在
// 操作結果:將L重置爲空表
Status ClearList(LinkList *L)
{
LinkList p,q;
p = (*L)->next; // p指向第一個結點
while(p)
{
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;// 頭結點指針域爲空
return Ok;
}
要知道p是一個結點,除了數據域還有指針域,free(p)是對整個結點進行刪除和內存釋放工作。所以如果程序中直接像下面這樣寫會出問題的(p的地址域已經被釋放)
free(p);
p = p->next;
2.5 單鏈表結構與順序存儲結構的比較
- 頻繁查找,很少進行插入刪除操作,宜採用順序存儲結構;反之用單鏈表。比如遊戲開發中,對於用戶註冊的個人信息,除了註冊時插入數據外,絕大多數情況都是讀取,所以應考慮用順序存儲結構。而遊戲中玩家的武器裝備列表,隨着玩家的遊戲過程中,可能隨時增加或刪除,單鏈表就可以大展拳腳了。
- 當線性表中元素個數變化較大或者根本不知道有多大時,最好用單鏈表結構,這樣就不需要考慮存儲空間大小問題。若事先知道大致長度,如一年12個月,這種用順序結構效率會高很多。
2.6 循環鏈表
對於非空的循環鏈表如下:
其實循環鏈表和單鏈表的主要差異就在於循環的判斷條件上,原來是判斷p->next是否爲空,現在是它不等於頭結點,則循環結束。
2.7 雙向鏈表
雙向鏈表是在單鏈表的每個結點中,再設置一個指向其前驅結點的指針域。
// 線性表的雙向鏈表存儲結構
typedef struct DulNode
{
ElemType data;
struct DulNode *prior; // 直接前驅指針
struct DulNode *next; // 直接後繼指針
}DulNode, *DuLinkList;
對於雙向鏈表中的一個結點p,它的後繼的前驅,以及前驅的後繼都是自己。
2.7.1 插入操作
插入操作時,並不複雜,但是順序很重要,不能寫反了。
s->prior = p; // 把p賦值給s的前驅,如圖中的1
s->next = p->next; // 把p->next賦值給s的後繼,如圖中2
p->next->prior = s; // 把s賦值給p->next的前驅,如圖中3
p->next = s; // 把s賦值給p的後繼,如圖中的4
2.7.2 刪除操作
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);