文章目錄
2.1 線性表的定義與基本操作
2.1.1 線性表的定義
線性表是具有相同
數據類型的n (n≥0) 個數據元素
的有限序列
,其中n爲表長
,當n=0時線性表是一個空表
。若用L命名線性表,則其一般表示爲
2.1.2 線性表的基本操作
線性表的基本操作:
2.1.3 小結
2.2 線性表的順序表示
2.2.1 順序表的定義
線性表的順序存儲又稱順序表。它是用一種地址連續的存儲單元存儲數據元素,使得邏輯上相鄰的數據元素物理上也相鄰。
(用存儲位置的相鄰來體現數據元素之間的邏輯關係)
注意:線性表中元素的位序是從1開始的,而數組中元素的下標是從0開始的。
1. 靜態分配
#define MaxSize 10 //定義最大長度
typedef struct{
ElemType data[MaxSize]; //用數組存放數據元素
int length; //順序表的當前長度
}SqList; //順序表的類型定義
//初始化一個順序表
//避免內存中會遺留“髒數據”
void InitList(SqList &L){
for(int i=0;i<MaxSize;i++)
L.data[i] = 0;
L.length = 0;
}
int main()
{
SqList L; //聲明一個順序表
InitList(L); //初始化順序表
return 0;
}
2. 動態分配
#include <stdio.h>
#include <stdlib.h> //malloc、free函數的頭文件
#define InitSize 10 //表長度的初始定義
typedef struct{
ElemType *data; //指示動態分配數組的指針
int MaxSize; //數組的最大容量
int length; //順序表的當前長度
}SqList; //順序表的類型定義
void InitList(SqList &L){
L.data = (ElemType *)malloc(sizeof(ElemType)*InitSize);
L.length = 0;
L.MaxSize = InitSize;
}
//增加動態數組的長度
void IncreaseSize(SqList &L,int len){
ElemType *p = L.data;
L.data = (ElemType *)malloc(sizeof(ElemType)*(MaxSize+len)); //重新分配了15個存儲單元
for(int i=0;i<L.length;i++)
L.data[i] = p[i]; //將數據複製到新區域(時間開銷大)
L.MaxSize = L.MaxSize+len;
free(p);
}
int main()
{
SqList L; //聲明一個順序表
InitList(L); //初始化順序表
//.....隨便插入幾個元素
IncreaseSize(L,5);
return 0;
}
3. 順序表的特點
4. 小結
2.2.2 順序表上基本操作的實現
1. 插入
ListInsert(&L,i,e):插入操作。在表L中的 第i個(i是位序)
位置上 插入指定元素e。
1 ≤ i ≤ L.length+1。
(插入、刪除代碼建立在順序表的“靜態分配”實現方式之上, “動態分配”也雷同。)
//在位序i插入元素e
bool ListInsert(SqList &L,int i,int e) {
if (i<1 || i>L.length + 1) return false;
if (L.length >= MaxSize) return false;
for (int j = L.length; j >= i; j--){
L.data[j] = L.data[j - 1];
}
L.data[i-1] = e;
L.length += 1;
return true;
}
2. 插入操作的時間複雜度分析
問題規模n= L.length (表長)
3. 刪除
ListDelete(&L,i,&e):刪除操作。刪除表L中 第i個(i是位序)
位置 的元素,並用e返回刪除元素的值。
1 ≤ i ≤ L.length。
//刪除位序i的元素,並用e帶回
bool ListDelete(SqList& L, int i, int& e) {
if (i<1 || i>L.length) return false;
e = L.data[i - 1];
for (int j = i;j < L.length; j++) {
L.data[j - 1] = L.data[j];
}
L.length--;
return true;
}
4. 刪除操作的時間複雜度分析
2.3 線性表的鏈式表示
2.3.1 單鏈表的定義
1. 什麼是單鏈表
2. 單鏈表中結點類型的描述以及單鏈表的表示
單鏈表中結點類型的描述:
typedef struct LNode {
int data;
struct LNode* next;
}LNode,*LinkList;
單鏈表的表示:
通常用頭指針標識一個單鏈表
LNode * L; //聲明一個指向單鏈表第一個結點的指針
或 LinkList L; //聲明一個指向單鏈表第一個結點的指針(代碼可讀性更強)
3.不帶頭結點的單鏈表
L= Null 是空表。
#include <stdio.h>
typedef struct LNode {
int data;
struct LNode* next;
}LNode,*LinkList;
//初始化一個空的單鏈表
bool InitList(LinkList &L) {
L = NULL; //防止髒數據
return true;
}
//判斷單鏈表是否爲空
bool IsEmpty(LinkList L) {
if (L == NULL) return true;
else return false;
}
void test() {
LinkList L; ////聲明一個指向單鏈表的指針
InitList(L); //初始化一個空表
//......後續代碼......
}
4. 帶頭結點的單鏈表
L->next == NULL是空表。
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode {
int data;
struct LNode* next;
}LNode,*LinkList;
//初始化一個空的單鏈表
bool InitList(LinkList &L) {
L = (LNode *)malloc(sizeof(LNode)); //分配一個頭結點
if (L == NULL) //malloc分配失敗
return false;
L->next = NULL;
return true;
}
//判斷單鏈表是否爲空
bool IsEmpty(LinkList L) {
if (L->next == NULL) return true;
else return false;
}
void test() {
LinkList L; ////聲明一個指向單鏈表的指針
InitList(L); //初始化一個空表
//......後續代碼......
}
5. 小結
2.3.2 單鏈表上基本操作的實現
一、單鏈表的插入刪除
1. 按位序插入
插入結點操作將值爲x的新結點插入到單鏈表的第i個位置上。先檢查插入位置的合法性,然後找到待插入位置的前驅結點,即第i-1個結點,在它後面插入結點。
(1) 帶頭結點
頭結點可以看作“第0個”結點(這樣寫代碼便於理解),j 初始設0
。
//在第i個位置插入元素e
bool ListInsert(LinkList &L,int i,int e) {
if (i < 1) //i值不合法
return false;
int j = 0; //當前p指向的結點位置
LNode* p; //指針p指向當前掃描到的結點
p = L;
while (j < i - 1 && p != NULL) { //循環找到第i-1個結點
p = p->next;
j++;
}
if (p == NULL) //i值不合法
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true; //插入成功
}
最好時間複雜度:O(1)
最壞時間複雜度:O(n)
平均時間複雜度:O(n)
(2) 不帶頭結點
不帶頭結點,那麼 j 初始設1
。
//在第i個位置插入元素e,不帶頭結點
bool ListInsert(LinkList& L, int i, int e) {
if (i < 1) //i值不合法
return false;
if (i == 1) { //插入第一個結點的操作與其他結點的操作不同
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = L;
L = s; //頭指針指向新結點
return true;
}
LNode* p; //指針p指向當前掃描到的結點
p = L;
int j = 1; //當前p指向的結點位置
while (j < i - 1 && p != NULL) { //循環找到第i-1個結點
p = p->next;
j++;
}
if (p == NULL) //i值不合法
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true; //插入成功
}
最好時間複雜度:O(1)
最壞時間複雜度:O(n)
平均時間複雜度:O(n)
2. 指定結點的後插操作
//在p結點之後插入元素e
bool InsertNextNode(LNode *p, int e) {
if (p == NULL)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL)//內存分配失敗
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
時間複雜度:O(1)
3. 指定結點的前插操作
想在指定結點前插的話,有兩種方法;
法一:
循環查找p的前驅結點q,再對q後插。時間複雜度是O(n)。
時間複雜度:O(n)
法二:
在p結點後插入結點s,然後將p結點的數據域與s結點的數據域交換。
//在p結點之前插入結點s
bool InsertPriorNode(LNode* p, LNode* s) {
if (p == NULL || s == NULL)
return false;
s->next = p->next;
p->next = s; //s連到p之後
int temp = p->data; //交換數據域部分
p->data = s->data;
s->data = temp;
return true;
}
時間複雜度:O(1)
4. 按位序刪除
(1) 帶頭結點的:
//刪除第i個位置的元素,並用e返回
bool ListDelete(LinkList& L, int i, int& e) {
if (i < 1) return false;
LNode* p; //p指向當前掃描到的結點
p = L;
int j = 0; //當前p指向的是第幾個結點
while (j < i - 1 && p != NULL) {
p = p->next;
j++;
}
if (p == NULL) //i值不合法
return false;
if (p->next == NULL) //第i-1個結點之後已無其他結點
return false;
LNode* q = p->next; //q指向被刪除的結點
e = q->data; //用e返回元素的值
p->next = q->next;
free(q); //釋放結點的存儲空間
return true; //刪除成功
}
最好時間複雜度:O(1)
最壞時間複雜度:O(n)
平均時間複雜度:O(n)
(2) 不帶頭結點的:
bool ListDelete(LinkList& L, int i, int& e) {
if (i < 1) return false;
LNode* p; //p指向當前掃描到的結點
p = L;
int j = 1; //當前p指向的是第幾個結點
if (i == 1) {
LNode* s;
s = L;
L = s->next;
free(s);
}
//.......剩下的與上面 帶頭結點代碼中while開始到結束 的一樣。
}
最好時間複雜度:O(1)
最壞時間複雜度:O(n)
平均時間複雜度:O(n)
5. 指定結點的刪除
刪除結點p,需要修改其前驅結點的next指針。有兩個方法。
法1:
傳入頭指針,循環尋找p的前驅結點。
時間複雜度:O(n)
法2:
將刪除結點的數據域與後驅結點的數據域交換,然後直接刪除後驅結點就好。
( 類似於指定結點前插的實現)
如果p是最後一個結點,不能 用方法二
,只能從表頭開始依次尋找p的前驅,使用方法一。
這裏可以發現單鏈表的侷限性:無法逆向檢索,有時候不太方便。
//刪除指定結點p
bool DeleteNode(LNode* p) {
if (p == NULL) return false;
LNode* q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
時間複雜度:O(1)
6. 小結
二、單鏈表的查找
1. 按位查找
時間複雜度:O(n)
2. 按值查找
//按值查找,找到數據域==e的結點
LNode* LocateElem(LinkList L, int e) {
LNode* p = L ->next;
//從第1個結點開始查找數據域爲e的結點
while (p != NULL && p->data != e)
p = p->next;
return p; //找到後返回該結 點指針,否則返回NULL
}
時間複雜度:O(n)
3. 求表的長度
求表長操作就是計算單鏈表中數據結點(不含頭結點)的個數。設置一個計數器len。
(1)帶頭結點的:
//求表的長度
int Length(LinkList L) {
int len = 0; //統計表長
LNode* p = L;
while (p ->next != NULL) {
p = p->next;
len++;
}
return len;
}
(2)不帶頭結點的:
只需要把len初始值改爲1即可。
(1) (2) 時間複雜度都是:O(n)
4. 小結
三、單鏈表的建立
如果給你很多個數據元素(ElemType),要把它們存到一個單鏈表裏邊,怎麼搞?
Step 1:初始化一個單鏈表
Step 2:每次取一個數據元素,插入到表尾/表頭
這裏只寫帶頭結點的。
1. 尾插法
//尾插法建立單鏈表
LinkList List_Taillnsert(LinkList& L) {
int x;
LNode *s,*r = L; //r是表尾指針
L = (LNode*)malloc(sizeof(LNode)); //頭結點
scanf_s("%d", &x); //輸入結點的值
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指向新的表尾結點;永遠保持r指向最後一個結點
scanf_s("%d", &x);
}
r->next = NULL; //尾結點指針置空
return L;
}
時間複雜度: O(n)
2. 頭插法
//頭插法
LinkList Head_Insert(LinkList &L) {
int x;
LNode* s;
L = (LNode*)malloc(sizeof(LNode)); //頭結點
L->next = NULL; //初始爲空鏈表
scanf_s("%d",&x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf_s("%d",&x);
}
return L;
}
時間複雜度: O(n)
3. 小結
2.3.3 雙鏈表
以下代碼都是基於帶頭結點的
單鏈表結點中只有一個指向其後繼的指針,使得單鏈表只能從頭結點依次順序地向後遍歷。要訪問某個結點的前驅結點(插入、刪除操作時),只能從頭開始遍歷,訪問後繼結點的時間複雜度爲O(1),訪問前驅結點的時間複雜度爲O(n)。
爲了克服單鏈表的上述缺點,引入了雙鏈表,
雙鏈表結點中有兩個指針 prior和next,分別指向其前驅結點和後驅結點。如下圖:
1. 雙鏈表的初始化
//雙鏈表
typedef struct DNode {
int data;
struct DNode* prior;
struct DNode* next;
}DNode,*DLinkList;
//雙鏈表的初始化
bool InitList(DLinkList &L) {
L = (DNode*)malloc(sizeof(DNode)); //頭結點
if (L == NULL) return false;
L->prior = NULL; //頭結點的prior永遠指向NULL
L->next = NULL; //頭結點目前後面沒有結點
return true;
}
//判斷雙鏈表是否爲空
bool Empty(DLinkList L) {
if (L->next == NULL) return true;
else return false;
}
2. 雙鏈表的插入
如果p是最後一個結點,就要加上一句 if(p->next != NULL) //如果p結點有後繼結點。
注意:1 2步必須在4之前。
//雙鏈表的插入,在p結點之後插入s結點
bool InsertNextDNode(DNode *p,DNode *s) {
if (p == NULL || s == NULL) return false;
s->next = p->next; // 1
if(p->next != NULL) //如果p結點有後繼結點
p->next->prior = s; // 2
s->prior = p; // 3
p->next = s; // 4
return true;
}
3. 雙鏈表的刪除
如果p是最後一個結點,就要加上一句 if(q->next != NULL) //q結點不是最後一個結點
//雙鏈表的刪除,刪除p結點的後繼節點
bool DeleteNextDNode(DNode* p) {
if (p == NULL) return false;
DNode* q = p->next; //要刪除的結點q
if (q == NULL) return false; //若p沒有後繼
p->next = q->next;
if(q->next != NULL) //q結點不是最後一個結點
q->next->prior = p;
free(q); //釋放結點空間
return true;
}
4. 雙鏈表的遍歷
5. 小結
2.3.4 循環鏈表
1. 循環單鏈表
循環單鏈表的初始化:
//初始化循環單鏈表
bool InitList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode)); //分配一個頭結點
if (L == NULL) return false;
L->next = L;
return true;
}
//判斷循環單鏈表是否爲空
bool Empty(LinkList L) {
if (L->next == L) return true;
else return true;
}
2. 循環雙鏈表
循環雙鏈表的初始化:
//初始化循環雙鏈表
bool InitDLinkList(DLinkList &L) {
L = (DNode*)malloc(sizeof(DNode)); //分配一個頭結點
if (L == NULL) return false;
L->prior = L;
L->next = L;
return true;
}
//判斷循環雙鏈表是否爲空
bool Empty(DLinkList L) {
if (L->next == L) return true;
else return false;
}
循環雙鏈表的插入:
循環雙鏈表的刪除:
3. 小結
2.3.5 靜態鏈表
1. 什麼是靜態鏈表
其實就是數組,只是每個位置存儲的是結點類型的元素,指針域放的是下一個元素的下標。
2. 如何定義一個靜態鏈表
3. 簡述基本操作的實現
4. 小結
2.3.6 順序表和鏈表的比較
1. 邏輯結構
2. 物理結構
3. 基本操作
4. 順序表與鏈表的適用場景
5. 知識回顧&&重要考點