跳到正文

线性表

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

王道《数据结构》第二章线性表
线性表位序从 1 开始,数组下标从 0 开始;链表默认采用带头结点写法。


一、公共头文件

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

C 语言动态内存管理:

LNode *p = (LNode *)malloc(sizeof(LNode));
free(p);

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


二、顺序表

1. 静态顺序表定义

#define MAX_SIZE 100

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

2. 动态顺序表定义

#define INIT_SIZE 100

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

3. 初始化

静态顺序表

void InitSqList(SqList *L) {
    L->length = 0;
}

调用:

SqList L;
InitSqList(&L);

动态顺序表

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. 动态顺序表扩容

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. 判空与求表长

bool SqListEmpty(const SqList *L) {
    return L->length == 0;
}

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

6. 按位查找

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. 按值查找

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 \]

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. 删除元素

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. 遍历

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

11. 销毁动态顺序表

void DestroySeqList(SeqList *L) {
    free(L->data);
    L->data = NULL;
    L->length = 0;
    L->maxSize = 0;
}

三、单链表

1. 结点定义

typedef struct LNode {
    int data;
    struct LNode *next;
} LNode, *LinkList;

其中 LinkList 等价于 LNode *

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

bool InitList(LinkList *L) {
    *L = (LNode *)malloc(sizeof(LNode));
    if (*L == NULL)
        return false;

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

调用:

LinkList L;
InitList(&L);

注意两层指针:

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

3. 判空与求表长

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 个结点。

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. 按值查找

LNode *LocateElemNode(LinkList L, int e) {
    LNode *p = L->next;

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

    return p;
}

6. 在指定结点后插入

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;
}

核心顺序:

s->next = p->next;
p->next = s;

7. 按位序插入

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

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 时实现前插

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. 删除指定结点的后继

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. 按位序删除

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 时删除该结点

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. 头插法建立单链表

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. 尾插法建立单链表

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. 遍历单链表

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

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

15. 销毁单链表

void DestroyList(LinkList *L) {
    LNode *p;

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

四、双链表

1. 结点定义

typedef struct DNode {
    int data;
    struct DNode *prior;
    struct DNode *next;
} DNode, *DLinkList;

2. 初始化

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

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 的后继结点

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

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. 循环单链表初始化

bool InitCircularList(LinkList *L) {
    *L = (LNode *)malloc(sizeof(LNode));
    if (*L == NULL)
        return false;

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

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

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. 循环双链表初始化

bool InitCircularDList(DLinkList *L) {
    *L = (DNode *)malloc(sizeof(DNode));
    if (*L == NULL)
        return false;

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

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

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. 定义

#define S_MAX_SIZE 100

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

typedef SNode SLinkList[S_MAX_SIZE];

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

2. 遍历

void PrintSLinkList(SLinkList L, int first) {
    int p = first;

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

3. 插入和删除

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 记录保留下来的元素个数。

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. 删除有序顺序表中的重复元素

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. 逆置顺序表

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. 合并两个递增顺序表

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 的结点

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. 单链表逆置——头插法

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. 单链表逆置——三指针法

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 个结点

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. 查找中间结点

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. 判断单链表是否有环

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. 寻找环的入口结点

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 的头结点作为结果表头。

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 中。

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. 删除递增单链表中的重复结点

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 保存原链表偶数位结点;
  • 两表中的相对次序均保持不变。
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 即可:

void ReverseByHeadInsert(LinkList L);
void DeleteAllXNode(LinkList L, int x);

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

2. 需要改变头指针本身

必须传入 LinkList *L

bool InitList(LinkList *L);
void DestroyList(LinkList *L);

调用时使用:

InitList(&L);
DestroyList(&L);

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

传入 int *e

int value;
ListDelete(&L, 2, &value);

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


十、常用指针操作口诀

1. 单链表后插

s->next = p->next;
p->next = s;

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

2. 单链表删除后继

q = p->next;
p->next = q->next;
free(q);

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

3. 双链表后插

s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;

4. 双链表删除后继

q = p->next;
p->next = q->next;
q->next->prior = p;
free(q);

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


十一、高频易错点

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

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

    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. 带头结点普通单链表的空表条件:

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

    L == NULL
    
  13. 带头结点循环单链表的空表条件:

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

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

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

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

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

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

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


posted @ 2026-08-06 19:55  尔一  阅读(12)  评论(0)    收藏  举报