鏈表

1. 常見的鏈表有單鏈表和雙鏈表

單鏈表

單鏈表的每個結點由用於存數據的 data 和指向下一個結點的指針 next 構成

typedef struct LNode{
    ElemType data;          // 單鏈表的數據域
    struct LNode *next;     // 單鏈表的指針域
} LNode, *LinkList;         // LinkList為指向LNode的指針類型

*LinkList 時,在定義單鏈表就可以寫成 LinkList L;若沒有,則只能寫成 LNode *L

雙鏈表

雙鏈表的每個結點由用於存數據的 data、指向上一個節點的指針 prev 以及指向下一個節點的指針 next 構成

typedef struct LNode{
    ElemType data;
    struct LNode *prev, *next;  // 指向上一個節點的指針prev和指向下一個節點的指針next
} DNode, *DLinkList;

2. 鏈式存儲和順序存儲

鏈式存儲是一種非連續的物理存儲結構,其中數據元素存儲在分散的節點(Node)中,每個節點包含:數據域和指針域。

順序存儲是一種連續的物理存儲結構,其中數據元素按邏輯順序依次存儲在一組地址連續的存儲單元中。通常通過數組(Array)實現,可以通過基地址 + 偏移量直接計算任何元素的存儲位置。

操作

鏈式存儲

順序存儲

説明

訪問第i個元素

O(n)

O(1)

鏈表需要從頭遍歷

插入/刪除(頭部)

O(1)

O(n)

數組需要移動元素

插入/刪除(中間)

O(n) 查找 + O(1) 操作

O(n) 移動

鏈表查找慢,操作快

插入/刪除(尾部)

O(n) 或 O(1)(有尾指針)

O(1)(容量足夠)或 O(n)(擴容)

內存利用率

較低(有指針開銷)

較高(只有數據)

鏈表每個節點都有指針開銷

緩存友好性

差(內存不連續)

好(內存連續)

CPU緩存預取對數組更有利

常見的順序存儲的實例就是數組和順序表。

下面以數組來講解順序存儲的元素在內存中的定位問題:

對於數組 ElemType a[]

如果想訪問第n個元素,可以直接使用下標索引 a[n-1]

這裏第n個元素在內存中的地址就是:

address_n = address_a[0] + sizeof(ElemType) * n

這裏補充一下數組的定義:

數組的定義有兩種方法

a. 靜態定義

ElemType data[MAXSIZE];

b. 動態定義

ElemType *data;
data = (ElemType *)malloc(sizeof(ElemType) * MAXSIZE);

這裏使用C++中的new操作符書寫:

ElemType* data = new ElemType[MAXSIZE];

關於內存分配函數的一些知識

函數

malloc

calloc

realloc

參數

size (字節數)

num (元素個數), size (元素大小)

ptr (原指針), new_size (新大小)

是否初始化

不初始化(內容是隨機的)

初始化為0

保持原有數據(新空間不初始化)

性能

較快

較慢

取決於情況

常用場景

一般內存分配

需要清零的數組

動態調整數組大小

int *arr = (int*)malloc(n * sizeof(int));      // 分配n個整數的空間

int *arr = (int*)calloc(n, sizeof(int));      // 分配並清零一個整數數組

int *arr = (int*)malloc(3 * sizeof(int));
int *temp = (int*)realloc(arr, 5 * sizeof(int));  // 拓展原數組至5個整數的空間

3. 頭指針,頭結點和首元結點

頭指針:指向鏈表中第一個結點的指針

首元結點:鏈表中存儲第一個數據元素a₁的結點

頭結點:在鏈表的首元結點前附設的一個結點,data域為空

4. 單鏈表建立的兩種方法

a. 頭插法

算法思路:

  1. 從一個空表開始,重複讀入數據
  2. 生成新的結點,將讀入的數據存放到新結點的數據域中
  3. 從最後一個結點開始,依次將各節點插入到鏈表前端
void CreateList_H(LinkList &L, int n){
    L = new LNode;
    L -> next = NULL;
    for(int i = n; i > 0; i--){
        p = new LNode;
        cin >> p -> data;
        p -> next = L -> next;
        L -> next = p;
    }
}

b. 尾插法

算法思路:

  1. 從一個空表L開始,將新結點逐個插入到鏈表尾部,尾指針r指向鏈表的尾結點
  2. 初始時,r同L均指向頭結點,每讀入一個數據元素則申請一個新結點,將新結點插入到尾結點後,r指向新結點
void CreateList_R(LinkList &L, int n){
    L = new LNode;
    L -> next = NULL;
    r = L;
    for(int i = 0; i < n; i++){
        p = new LNode;
        cin >> p -> data;
        p -> next = NULL;
        r -> next = p;
        r = p;
    }
}