线性表

线性表常用操作与算法代码总结(C 语言版)

王道《数据结构》第二章线性表

线性表位序从 1 开始,数组下标从 0 开始;链表默认采用带头结点写法。


一、公共头文件

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

C 语言动态内存管理:

c 复制代码
LNode *p = (LNode *)malloc(sizeof(LNode));
free(p);

malloc 申请失败时返回 NULL。申请的结点使用完后必须用 free 释放。


二、顺序表

1. 静态顺序表定义

c 复制代码
#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int length;
} SqList;

2. 动态顺序表定义

c 复制代码
#define INIT_SIZE 100

typedef struct {
    int *data;
    int length;
    int maxSize;
} SeqList;

3. 初始化

静态顺序表

c 复制代码
void InitSqList(SqList *L) {
    L->length = 0;
}

调用:

c 复制代码
SqList L;
InitSqList(&L);

动态顺序表

c 复制代码
bool InitSeqList(SeqList *L) {
    L->data = (int *)malloc(INIT_SIZE * sizeof(int));
    if (L->data == NULL)
        return false;

    L->length = 0;
    L->maxSize = INIT_SIZE;
    return true;
}

4. 动态顺序表扩容

c 复制代码
bool IncreaseSize(SeqList *L, int increase) {
    if (increase <= 0)
        return false;

    int newSize = L->maxSize + increase;
    int *newData = (int *)malloc(newSize * sizeof(int));
    if (newData == NULL)
        return false;

    for (int i = 0; i < L->length; i++)
        newData[i] = L->data[i];

    free(L->data);
    L->data = newData;
    L->maxSize = newSize;
    return true;
}

时间复杂度:\(O(n)\)。

5. 判空与求表长

c 复制代码
bool SqListEmpty(const SqList *L) {
    return L->length == 0;
}

int SqListLength(const SqList *L) {
    return L->length;
}

6. 按位查找

c 复制代码
bool GetElem(const SqList *L, int i, int *e) {
    if (i < 1 || i > L->length)
        return false;

    *e = L->data[i - 1];
    return true;
}

核心关系:

\\\text{第 }i\\text{ 个元素}\\longleftrightarrow \\texttt{data\[i-1} \]

时间复杂度:\(O(1)\)。

7. 按值查找

c 复制代码
int LocateElem(const SqList *L, int e) {
    for (int i = 0; i < L->length; i++) {
        if (L->data[i] == e)
            return i + 1;
    }
    return 0;
}

时间复杂度:\(O(n)\)。

8. 插入元素

在第 i 个位置插入 e,合法范围为:

\1\\le i\\le L.length+1 \\

c 复制代码
bool ListInsert(SqList *L, int i, int e) {
    if (i < 1 || i > L->length + 1)
        return false;
    if (L->length >= MAX_SIZE)
        return false;

    for (int j = L->length; j >= i; j--)
        L->data[j] = L->data[j - 1];

    L->data[i - 1] = e;
    L->length++;
    return true;
}

插入时从后向前移动,防止原元素被覆盖。

9. 删除元素

c 复制代码
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;
}

删除时从前向后移动。

10. 遍历

c 复制代码
void PrintSqList(const SqList *L) {
    for (int i = 0; i < L->length; i++)
        printf("%d ", L->data[i]);
    printf("\n");
}

11. 销毁动态顺序表

c 复制代码
void DestroySeqList(SeqList *L) {
    free(L->data);
    L->data = NULL;
    L->length = 0;
    L->maxSize = 0;
}

三、单链表

1. 结点定义

c 复制代码
typedef struct LNode {
    int data;
    struct LNode *next;
} LNode, *LinkList;

其中 LinkList 等价于 LNode *

2. 初始化带头结点单链表

c 复制代码
bool InitList(LinkList *L) {
    *L = (LNode *)malloc(sizeof(LNode));
    if (*L == NULL)
        return false;

    (*L)->next = NULL;
    return true;
}

调用:

c 复制代码
LinkList L;
InitList(&L);

注意两层指针:

  • L 是头指针变量;
  • &L 是头指针变量的地址;
  • 函数中用 *L 修改外部头指针。

3. 判空与求表长

c 复制代码
bool ListEmpty(LinkList L) {
    return L->next == NULL;
}

int ListLength(LinkList L) {
    int length = 0;
    LNode *p = L->next;

    while (p != NULL) {
        length++;
        p = p->next;
    }
    return length;
}

4. 按位查找

带头结点时,头结点看作第 0 个结点。

c 复制代码
LNode *GetElemNode(LinkList L, int i) {
    if (i < 0)
        return NULL;

    LNode *p = L;
    int j = 0;

    while (p != NULL && j < i) {
        p = p->next;
        j++;
    }
    return p;
}
  • GetElemNode(L, 0) 返回头结点;
  • GetElemNode(L, 1) 返回第一个数据结点;
  • 时间复杂度为 \(O(n)\)。

5. 按值查找

c 复制代码
LNode *LocateElemNode(LinkList L, int e) {
    LNode *p = L->next;

    while (p != NULL && p->data != e)
        p = p->next;

    return p;
}

6. 在指定结点后插入

c 复制代码
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;
}

核心顺序:

c 复制代码
s->next = p->next;
p->next = s;

7. 按位序插入

在第 i 个位置插入,需要先找到第 i-1 个结点。

c 复制代码
bool LinkListInsert(LinkList L, int i, int e) {
    if (i < 1)
        return false;

    LNode *p = GetElemNode(L, i - 1);
    if (p == NULL)
        return false;

    return InsertNextNode(p, e);
}

8. 只知道结点 p 时实现前插

c 复制代码
bool InsertPriorNode(LNode *p, int e) {
    if (p == NULL)
        return false;

    LNode *s = (LNode *)malloc(sizeof(LNode));
    if (s == NULL)
        return false;

    s->next = p->next;
    p->next = s;
    s->data = p->data;
    p->data = e;
    return true;
}

本质:先在 p 后插,再交换数据,时间复杂度为 \(O(1)\)。

9. 删除指定结点的后继

c 复制代码
bool DeleteNextNode(LNode *p, int *e) {
    if (p == NULL || p->next == NULL)
        return false;

    LNode *q = p->next;
    *e = q->data;
    p->next = q->next;
    free(q);
    return true;
}

10. 按位序删除

c 复制代码
bool LinkListDelete(LinkList L, int i, int *e) {
    if (i < 1)
        return false;

    LNode *p = GetElemNode(L, i - 1);
    if (p == NULL)
        return false;

    return DeleteNextNode(p, e);
}

11. 只知道结点 p 时删除该结点

c 复制代码
bool DeleteNode(LNode *p) {
    if (p == NULL || p->next == NULL)
        return false;

    LNode *q = p->next;
    p->data = q->data;
    p->next = q->next;
    free(q);
    return true;
}

此方法不能删除尾结点,因为尾结点没有后继。

12. 头插法建立单链表

c 复制代码
bool ListHeadInsert(LinkList *L, int n) {
    if (!InitList(L))
        return false;

    for (int i = 0; i < n; i++) {
        int x;
        scanf("%d", &x);

        LNode *s = (LNode *)malloc(sizeof(LNode));
        if (s == NULL)
            return false;

        s->data = x;
        s->next = (*L)->next;
        (*L)->next = s;
    }
    return true;
}

输入顺序与链表最终顺序相反,时间复杂度为 \(O(n)\)。

13. 尾插法建立单链表

c 复制代码
bool ListTailInsert(LinkList *L, int n) {
    if (!InitList(L))
        return false;

    LNode *rear = *L;

    for (int i = 0; i < n; i++) {
        int x;
        scanf("%d", &x);

        LNode *s = (LNode *)malloc(sizeof(LNode));
        if (s == NULL)
            return false;

        s->data = x;
        s->next = NULL;
        rear->next = s;
        rear = s;
    }
    return true;
}

输入顺序与链表最终顺序相同,时间复杂度为 \(O(n)\)。

14. 遍历单链表

c 复制代码
void PrintList(LinkList L) {
    LNode *p = L->next;

    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

15. 销毁单链表

c 复制代码
void DestroyList(LinkList *L) {
    LNode *p;

    while (*L != NULL) {
        p = *L;
        *L = (*L)->next;
        free(p);
    }
}

四、双链表

1. 结点定义

c 复制代码
typedef struct DNode {
    int data;
    struct DNode *prior;
    struct DNode *next;
} DNode, *DLinkList;

2. 初始化

c 复制代码
bool InitDLinkList(DLinkList *L) {
    *L = (DNode *)malloc(sizeof(DNode));
    if (*L == NULL)
        return false;

    (*L)->prior = NULL;
    (*L)->next = NULL;
    return true;
}

3. 在 p 后插入结点 s

c 复制代码
bool InsertNextDNode(DNode *p, DNode *s) {
    if (p == NULL || s == NULL)
        return false;

    s->next = p->next;
    if (p->next != NULL)
        p->next->prior = s;
    s->prior = p;
    p->next = s;
    return true;
}

4. 删除 p 的后继结点

c 复制代码
bool DeleteNextDNode(DNode *p, int *e) {
    if (p == NULL || p->next == NULL)
        return false;

    DNode *q = p->next;
    *e = q->data;
    p->next = q->next;

    if (q->next != NULL)
        q->next->prior = p;

    free(q);
    return true;
}

5. 删除已知结点 p

c 复制代码
bool DeleteDNode(DNode *p) {
    if (p == NULL || p->prior == NULL)
        return false;

    p->prior->next = p->next;
    if (p->next != NULL)
        p->next->prior = p->prior;

    free(p);
    return true;
}

已知待删除结点地址时,双链表删除的时间复杂度为 \(O(1)\)。


五、循环链表

1. 循环单链表初始化

c 复制代码
bool InitCircularList(LinkList *L) {
    *L = (LNode *)malloc(sizeof(LNode));
    if (*L == NULL)
        return false;

    (*L)->next = *L;
    return true;
}

2. 循环单链表判空、判尾和遍历

c 复制代码
bool CircularListEmpty(LinkList L) {
    return L->next == L;
}

bool IsTail(LinkList L, LNode *p) {
    return p->next == L;
}

void PrintCircularList(LinkList L) {
    LNode *p = L->next;

    while (p != L) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

普通单链表遍历到 NULL 结束;带头结点循环单链表重新回到 L 时结束。

3. 循环双链表初始化

c 复制代码
bool InitCircularDList(DLinkList *L) {
    *L = (DNode *)malloc(sizeof(DNode));
    if (*L == NULL)
        return false;

    (*L)->prior = *L;
    (*L)->next = *L;
    return true;
}

4. 循环双链表插入和删除

c 复制代码
bool InsertNextCircularDNode(DNode *p, DNode *s) {
    if (p == NULL || s == NULL)
        return false;

    s->next = p->next;
    p->next->prior = s;
    s->prior = p;
    p->next = s;
    return true;
}

bool DeleteNextCircularDNode(DLinkList L, DNode *p, int *e) {
    if (p == NULL || p->next == L)
        return false;

    DNode *q = p->next;
    *e = q->data;
    p->next = q->next;
    q->next->prior = p;
    free(q);
    return true;
}

六、静态链表

1. 定义

c 复制代码
#define S_MAX_SIZE 100

typedef struct {
    int data;
    int next;
} SNode;

typedef SNode SLinkList[S_MAX_SIZE];

next 保存的是下一个结点的数组下标,而不是真实地址。通常用 -1 表示链尾。

2. 遍历

c 复制代码
void PrintSLinkList(SLinkList L, int first) {
    int p = first;

    while (p != -1) {
        printf("%d ", L[p].data);
        p = L[p].next;
    }
    printf("\n");
}

3. 插入和删除

c 复制代码
void InsertNextSNode(SLinkList L, int p, int s) {
    L[s].next = L[p].next;
    L[p].next = s;
}

int DeleteNextSNode(SLinkList L, int p) {
    int q = L[p].next;
    if (q == -1)
        return -1;

    L[p].next = L[q].next;
    return q;
}

静态链表删除结点后,还应按照具体实现把下标 q 归还给备用链表。


七、高频算法题模板

1. 删除顺序表中所有值为 x 的元素

k 记录保留下来的元素个数。

c 复制代码
void DeleteAllX(SqList *L, int x) {
    int k = 0;

    for (int i = 0; i < L->length; i++) {
        if (L->data[i] != x)
            L->data[k++] = L->data[i];
    }
    L->length = k;
}

时间复杂度 \(O(n)\),空间复杂度 \(O(1)\)。

2. 删除有序顺序表中的重复元素

c 复制代码
bool DeleteSame(SqList *L) {
    if (L->length == 0)
        return false;

    int k = 0;

    for (int i = 1; i < L->length; i++) {
        if (L->data[i] != L->data[k]) {
            k++;
            L->data[k] = L->data[i];
        }
    }

    L->length = k + 1;
    return true;
}

3. 逆置顺序表

c 复制代码
void ReverseSqList(SqList *L) {
    for (int i = 0; i < L->length / 2; i++) {
        int temp = L->data[i];
        L->data[i] = L->data[L->length - 1 - i];
        L->data[L->length - 1 - i] = temp;
    }
}

4. 合并两个递增顺序表

c 复制代码
bool MergeSqList(const SqList *A, const SqList *B, SqList *C) {
    if (A->length + B->length > MAX_SIZE)
        return false;

    int i = 0, j = 0, k = 0;

    while (i < A->length && j < B->length) {
        if (A->data[i] <= B->data[j])
            C->data[k++] = A->data[i++];
        else
            C->data[k++] = B->data[j++];
    }

    while (i < A->length)
        C->data[k++] = A->data[i++];
    while (j < B->length)
        C->data[k++] = B->data[j++];

    C->length = k;
    return true;
}

时间复杂度:\(O(m+n)\)。

5. 删除单链表中所有值为 x 的结点

c 复制代码
void DeleteAllXNode(LinkList L, int x) {
    LNode *p = L;

    while (p->next != NULL) {
        if (p->next->data == x) {
            LNode *q = p->next;
            p->next = q->next;
            free(q);
        } else {
            p = p->next;
        }
    }
}

删除成功后 p 不后移,因为新的 p->next 仍可能等于 x

6. 单链表逆置------头插法

c 复制代码
void ReverseByHeadInsert(LinkList L) {
    LNode *p = L->next;
    L->next = NULL;

    while (p != NULL) {
        LNode *q = p->next;
        p->next = L->next;
        L->next = p;
        p = q;
    }
}

7. 单链表逆置------三指针法

c 复制代码
void ReverseByPointers(LinkList L) {
    LNode *pre = NULL;
    LNode *cur = L->next;

    while (cur != NULL) {
        LNode *next = cur->next;
        cur->next = pre;
        pre = cur;
        cur = next;
    }

    L->next = pre;
}

8. 查找倒数第 k 个结点

c 复制代码
LNode *FindLastK(LinkList L, int k) {
    if (k <= 0)
        return NULL;

    LNode *fast = L->next;
    LNode *slow = L->next;

    for (int i = 0; i < k; i++) {
        if (fast == NULL)
            return NULL;
        fast = fast->next;
    }

    while (fast != NULL) {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

9. 查找中间结点

c 复制代码
LNode *FindMiddle(LinkList L) {
    LNode *slow = L->next;
    LNode *fast = L->next;

    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

当结点数为偶数时,此写法返回两个中间结点中的后一个。

10. 判断单链表是否有环

c 复制代码
bool HasCycle(LinkList L) {
    LNode *slow = L;
    LNode *fast = L;

    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;

        if (slow == fast)
            return true;
    }
    return false;
}

11. 寻找环的入口结点

c 复制代码
LNode *FindCycleEntry(LinkList L) {
    LNode *slow = L;
    LNode *fast = L;

    do {
        if (fast == NULL || fast->next == NULL)
            return NULL;
        slow = slow->next;
        fast = fast->next->next;
    } while (slow != fast);

    slow = L;
    while (slow != fast) {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

12. 合并两个递增单链表,结果仍递增

复用原有结点,以 A 的头结点作为结果表头。

c 复制代码
LinkList MergeListAscending(LinkList A, LinkList B) {
    LNode *pa = A->next;
    LNode *pb = B->next;
    LNode *tail = A;

    while (pa != NULL && pb != NULL) {
        if (pa->data <= pb->data) {
            tail->next = pa;
            pa = pa->next;
        } else {
            tail->next = pb;
            pb = pb->next;
        }
        tail = tail->next;
    }

    tail->next = (pa != NULL) ? pa : pb;
    free(B);
    return A;
}

13. 合并两个递增单链表,结果递减

比较两表当前较小结点,并将其头插到 A 中。

c 复制代码
LinkList MergeListDescending(LinkList A, LinkList B) {
    LNode *pa = A->next;
    LNode *pb = B->next;
    A->next = NULL;

    while (pa != NULL && pb != NULL) {
        LNode *p;

        if (pa->data <= pb->data) {
            p = pa;
            pa = pa->next;
        } else {
            p = pb;
            pb = pb->next;
        }

        p->next = A->next;
        A->next = p;
    }

    LNode *remain = (pa != NULL) ? pa : pb;
    while (remain != NULL) {
        LNode *p = remain;
        remain = remain->next;
        p->next = A->next;
        A->next = p;
    }

    free(B);
    return A;
}

14. 删除递增单链表中的重复结点

c 复制代码
void DeleteDuplicate(LinkList L) {
    LNode *p = L->next;

    while (p != NULL && p->next != NULL) {
        if (p->data == p->next->data) {
            LNode *q = p->next;
            p->next = q->next;
            free(q);
        } else {
            p = p->next;
        }
    }
}

15. 将单链表拆分成奇数位表和偶数位表

  • A 保留原链表奇数位结点;
  • B 保存原链表偶数位结点;
  • 两表中的相对次序均保持不变。
c 复制代码
bool SplitByPosition(LinkList A, LinkList *B) {
    *B = (LNode *)malloc(sizeof(LNode));
    if (*B == NULL)
        return false;

    (*B)->next = NULL;

    LNode *tailA = A;
    LNode *tailB = *B;
    LNode *p = A->next;
    int position = 1;

    A->next = NULL;

    while (p != NULL) {
        LNode *next = p->next;
        p->next = NULL;

        if (position % 2 == 1) {
            tailA->next = p;
            tailA = p;
        } else {
            tailB->next = p;
            tailB = p;
        }

        position++;
        p = next;
    }
    return true;
}

八、复杂度速查表

1. 基本操作

操作 顺序表 单链表 双链表
按位访问 \(O(1)\) \(O(n)\) \(O(n)\)
按值查找 \(O(n)\) \(O(n)\) \(O(n)\)
按位序插入 \(O(n)\) \(O(n)\) \(O(n)\)
按位序删除 \(O(n)\) \(O(n)\) \(O(n)\)
已知结点后插 --- \(O(1)\) \(O(1)\)
已知前驱删除后继 --- \(O(1)\) \(O(1)\)
已知待删除结点 --- 通常 \(O(n)\) \(O(1)\)

链表插入、删除只有在相关结点地址已经给出时才是 \(O(1)\);如果还要按位查找,总复杂度仍为 \(O(n)\)。

2. 高频算法

算法 时间复杂度 额外空间
删除所有值为 x 的元素 \(O(n)\) \(O(1)\)
顺序表逆置 \(O(n)\) \(O(1)\)
单链表逆置 \(O(n)\) \(O(1)\)
查找倒数第 k 个结点 \(O(n)\) \(O(1)\)
查找中间结点 \(O(n)\) \(O(1)\)
判断是否有环 \(O(n)\) \(O(1)\)
合并两个有序表 \(O(m+n)\) \(O(1)\)
删除有序表重复元素 \(O(n)\) \(O(1)\)
拆分单链表 \(O(n)\) \(O(1)\)

九、C 语言指针参数速记

1. 只读取或修改头结点之后的链接

传入 LinkList L 即可:

c 复制代码
void ReverseByHeadInsert(LinkList L);
void DeleteAllXNode(LinkList L, int x);

因为虽然形参 L 是指针的副本,但 L 和外部头指针仍指向同一个头结点。

2. 需要改变头指针本身

必须传入 LinkList *L

c 复制代码
bool InitList(LinkList *L);
void DestroyList(LinkList *L);

调用时使用:

c 复制代码
InitList(&L);
DestroyList(&L);

3. 用参数返回一个普通整数

传入 int *e

c 复制代码
int value;
ListDelete(&L, 2, &value);

函数内部通过 *e = ... 修改调用者的变量。


十、常用指针操作口诀

1. 单链表后插

c 复制代码
s->next = p->next;
p->next = s;

先让新结点接后面,再让前面接新结点。

2. 单链表删除后继

c 复制代码
q = p->next;
p->next = q->next;
free(q);

先保存,后断链,再释放。

3. 双链表后插

c 复制代码
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;

4. 双链表删除后继

c 复制代码
q = p->next;
p->next = q->next;
q->next->prior = p;
free(q);

普通双链表必须判断 q->next 是否为 NULL;循环双链表不需要。


十一、高频易错点

  1. C 语言没有引用参数,不能写 SqList &Lint &e

  2. 修改结构体变量本身时传地址:

    c 复制代码
    void InitSqList(SqList *L);
    InitSqList(&L);
  3. 结构体指针用 ->,结构体变量用 .

  4. C 语言使用 malloc/free,不使用 new/delete

  5. malloc 后要判断是否为 NULL

  6. free(p) 后不能继续访问 p 指向的内存。

  7. 顺序表第 i 个元素对应数组下标 i-1

  8. 顺序表插入的合法位置为 \(1\le i\le n+1\)。

  9. 顺序表删除的合法位置为 \(1\le i\le n\)。

  10. 顺序表插入从后向前移动,删除从前向后移动。

  11. 带头结点普通单链表的空表条件:

    c 复制代码
    L->next == NULL
  12. 不带头结点单链表的空表条件:

    c 复制代码
    L == NULL
  13. 带头结点循环单链表的空表条件:

    c 复制代码
    L->next == L
  14. 普通单链表尾结点条件为 p->next == NULL;循环单链表尾结点条件为 p->next == L

  15. 删除链表结点前必须先保存相关地址,否则可能断链。

  16. 删除结点后必须 free;仅修改指针会造成内存泄漏。

  17. 单链表已知结点 p 的地址,不代表已经知道它的前驱。

  18. 单链表"复制后继数据删除自身"的方法不能用于尾结点。

  19. 头插法建立链表会逆序;尾插法保持输入顺序。

  20. 动态顺序表扩容后仍然是顺序存储,元素地址仍连续。