线性表常用操作与算法代码总结

线性表常用操作与算法代码总结

适用范围:王道《数据结构》第二章线性表

代码语言:C++

约定:顺序表的位序从 1 开始,数组下标从 0 开始;链表默认采用带头结点的写法。


一、顺序表

1. 顺序表的定义

静态顺序表

cpp 复制代码
#include <iostream>
using namespace std;

const int MaxSize = 100;

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

动态顺序表

cpp 复制代码
const int InitSize = 100;

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

2. 初始化顺序表

静态顺序表

cpp 复制代码
void InitList(SqList &L) {
    L.length = 0;
}

动态顺序表

cpp 复制代码
void InitList(SeqList &L) {
    L.data = new int[InitSize];
    L.length = 0;
    L.maxSize = InitSize;
}

时间复杂度:

\O(1) \\


3. 动态顺序表扩容

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

    int *newData = new int[L.maxSize + increase];

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

    delete[] L.data;
    L.data = newData;
    L.maxSize += increase;

    return true;
}

时间复杂度:

\O(n) \\

动态分配只表示容量可以扩充,顺序表中的元素仍然连续存放。


4. 判断顺序表是否为空

cpp 复制代码
bool Empty(SqList L) {
    return L.length == 0;
}

5. 求表长

cpp 复制代码
int Length(SqList L) {
    return L.length;
}

6. 按位查找

查找位序为 i 的元素,并通过 e 返回。

cpp 复制代码
bool GetElem(SqList L, int i, int &e) {
    if (i < 1 || i > L.length)
        return false;

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

时间复杂度:

\O(1) \\

核心对应关系:

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


7. 按值查找

返回第一个值等于 e 的元素的位序;查找失败返回 0

cpp 复制代码
int LocateElem(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 \\

cpp 复制代码
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++;

    return true;
}

移动方向:从后向前。

text 复制代码
原表:10 20 30 40
在第2个位置插入99:

10 20 30 40 40
10 20 30 30 40
10 20 20 30 40
10 99 20 30 40

时间复杂度:

情况 插入位置 复杂度
最好 表尾 \(O(1)\)
最坏 表头 \(O(n)\)
平均 任意位置 \(O(n)\)

平均移动次数:

\\\frac{n}{2} \\


9. 删除元素

删除第 i 个元素,并通过 e 返回被删除的值。

合法范围:

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

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

移动方向:从前向后。

时间复杂度:

情况 删除位置 复杂度
最好 表尾 \(O(1)\)
最坏 表头 \(O(n)\)
平均 任意位置 \(O(n)\)

平均移动次数:

\\\frac{n-1}{2} \\


10. 遍历顺序表

cpp 复制代码
void PrintList(SqList L) {
    for (int i = 0; i < L.length; i++)
        cout << L.data[i] << ' ';

    cout << '\n';
}

时间复杂度:

\O(n) \\


11. 销毁动态顺序表

cpp 复制代码
void DestroyList(SeqList &L) {
    delete[] L.data;
    L.data = nullptr;
    L.length = 0;
    L.maxSize = 0;
}

二、单链表

1. 单链表结点定义

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

带头结点单链表:

text 复制代码
头指针L → 头结点 → 第1个数据结点 → 第2个数据结点 → NULL

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

cpp 复制代码
bool InitList(LinkList &L) {
    L = new LNode;

    if (L == nullptr)
        return false;

    L->next = nullptr;
    return true;
}

空表判断:

cpp 复制代码
bool Empty(LinkList L) {
    return L->next == nullptr;
}

3. 求单链表长度

cpp 复制代码
int Length(LinkList L) {
    int len = 0;
    LNode *p = L->next;

    while (p != nullptr) {
        len++;
        p = p->next;
    }

    return len;
}

时间复杂度:

\O(n) \\


4. 按位查找

在带头结点单链表中:

  • 头结点看作第 0 个结点;
  • 第一个数据结点是第 1 个结点。
cpp 复制代码
LNode *GetElem(LinkList L, int i) {
    if (i < 0)
        return nullptr;

    LNode *p = L;
    int j = 0;

    while (p != nullptr && j < i) {
        p = p->next;
        j++;
    }

    return p;
}

特殊情况:

cpp 复制代码
GetElem(L, 0);  // 返回头结点
GetElem(L, 1);  // 返回第一个数据结点

时间复杂度:

\O(n) \\


5. 按值查找

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

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

    return p;
}

时间复杂度:

\O(n) \\


6. 在指定结点后插入

已知结点 p,在 p 后面插入元素 e

cpp 复制代码
bool InsertNextNode(LNode *p, int e) {
    if (p == nullptr)
        return false;

    LNode *s = new LNode;
    s->data = e;

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

    return true;
}

核心语句:

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

必须先让 s 连接原来的后继,再让 p 指向 s,否则可能断链。

时间复杂度:

\O(1) \\


7. 按位序插入

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

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

    LNode *p = GetElem(L, i - 1);

    if (p == nullptr)
        return false;

    return InsertNextNode(p, e);
}

时间复杂度:

\O(n) \\

修改指针只需 \(O(1)\),但寻找第 i-1 个结点需要 \(O(n)\)。


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

通过"后插新结点 + 交换数据"模拟在 p 前面插入元素 e

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

    LNode *s = new LNode;

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

    s->data = p->data;
    p->data = e;

    return true;
}

时间复杂度:

\O(1) \\


9. 删除指定结点的后继

删除 p 的后继结点,并通过 e 返回其值。

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

    LNode *q = p->next;
    e = q->data;
    p->next = q->next;
    delete q;

    return true;
}

时间复杂度:

\O(1) \\


10. 按位序删除

删除第 i 个数据结点。

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

    LNode *p = GetElem(L, i - 1);

    if (p == nullptr)
        return false;

    return DeleteNextNode(p, e);
}

时间复杂度:

\O(n) \\


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

将后继结点的数据复制到 p,再删除后继结点。

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

    LNode *q = p->next;
    p->data = q->data;
    p->next = q->next;
    delete q;

    return true;
}

时间复杂度:

\O(1) \\

限制:不能用这种方法删除尾结点,因为尾结点没有后继。


12. 头插法建立单链表

cpp 复制代码
LinkList ListHeadInsert(LinkList &L, int n) {
    L = new LNode;
    L->next = nullptr;

    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;

        LNode *s = new LNode;
        s->data = x;

        s->next = L->next;
        L->next = s;
    }

    return L;
}

输入:

text 复制代码
1 2 3 4

结果:

text 复制代码
4 → 3 → 2 → 1

特点:建立后的链表顺序与输入顺序相反。

时间复杂度:

\O(n) \\


13. 尾插法建立单链表

cpp 复制代码
LinkList ListTailInsert(LinkList &L, int n) {
    L = new LNode;
    L->next = nullptr;

    LNode *r = L;

    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;

        LNode *s = new LNode;
        s->data = x;
        s->next = nullptr;

        r->next = s;
        r = s;
    }

    return L;
}

输入:

text 复制代码
1 2 3 4

结果:

text 复制代码
1 → 2 → 3 → 4

特点:建立后的链表顺序与输入顺序相同。

时间复杂度:

\O(n) \\

如果不设置尾指针 r,每次插入前都重新寻找尾结点,建表复杂度会变为 \(O(n^2)\)。


14. 遍历单链表

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

    while (p != nullptr) {
        cout << p->data << ' ';
        p = p->next;
    }

    cout << '\n';
}

15. 销毁单链表

包括头结点在内,逐个释放所有结点。

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

    while (L != nullptr) {
        p = L;
        L = L->next;
        delete p;
    }
}

时间复杂度:

\O(n) \\


三、双链表

1. 双链表结点定义

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

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

cpp 复制代码
bool InitDLinkList(DLinkList &L) {
    L = new DNode;

    if (L == nullptr)
        return false;

    L->prior = nullptr;
    L->next = nullptr;
    return true;
}

空表判断:

cpp 复制代码
bool Empty(DLinkList L) {
    return L->next == nullptr;
}

3. 在结点 p 后插入结点 s

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

    s->next = p->next;

    if (p->next != nullptr)
        p->next->prior = s;

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

    return true;
}

四步指针操作:

cpp 复制代码
s->next = p->next;
p->next->prior = s;  // 仅当p存在后继时执行
s->prior = p;
p->next = s;

时间复杂度:

\O(1) \\


4. 删除 p 的后继结点

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

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

    p->next = q->next;

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

    delete q;
    return true;
}

时间复杂度:

\O(1) \\


5. 删除已知结点 p

双链表可以通过 prior 直接找到前驱。

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

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

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

    delete p;
    return true;
}

时间复杂度:

\O(1) \\


6. 向后遍历

cpp 复制代码
void PrintForward(DLinkList L) {
    DNode *p = L->next;

    while (p != nullptr) {
        cout << p->data << ' ';
        p = p->next;
    }

    cout << '\n';
}

7. 向前遍历

若已知尾结点 tail

cpp 复制代码
void PrintBackward(DNode *tail, DLinkList L) {
    DNode *p = tail;

    while (p != L) {
        cout << p->data << ' ';
        p = p->prior;
    }

    cout << '\n';
}

四、循环单链表

1. 初始化

cpp 复制代码
bool InitCircularList(LinkList &L) {
    L = new LNode;

    if (L == nullptr)
        return false;

    L->next = L;
    return true;
}

2. 判断是否为空

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

3. 判断结点是否为尾结点

cpp 复制代码
bool IsTail(LinkList L, LNode *p) {
    return p->next == L;
}

4. 遍历循环单链表

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

    while (p != L) {
        cout << p->data << ' ';
        p = p->next;
    }

    cout << '\n';
}

循环链表不能使用 p != nullptr 作为结束条件,否则会形成死循环。


5. 带尾指针的循环单链表尾插

rear 始终指向尾结点。空表时,rear 指向头结点。

cpp 复制代码
bool TailInsertCircular(LinkList &rear, int e) {
    if (rear == nullptr)
        return false;

    LNode *s = new LNode;
    s->data = e;

    s->next = rear->next;  // rear->next是头结点
    rear->next = s;
    rear = s;

    return true;
}

时间复杂度:

\O(1) \\

带尾指针循环单链表的优势:

  • 找尾结点:\(O(1)\);
  • 找头结点:rear->next,\(O(1)\);
  • 找第一个数据结点:rear->next->next,\(O(1)\)。

五、循环双链表

1. 初始化

cpp 复制代码
bool InitCircularDList(DLinkList &L) {
    L = new DNode;

    if (L == nullptr)
        return false;

    L->prior = L;
    L->next = L;
    return true;
}

空表时:

cpp 复制代码
L->next == L;
L->prior == L;

2. 判断是否为空

cpp 复制代码
bool EmptyCircularDList(DLinkList L) {
    return L->next == L;
}

3. 在 p 后插入 s

循环双链表中每个结点都有前驱和后继,不需要判断 nullptr

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

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

    return true;
}

4. 删除 p 的后继结点

cpp 复制代码
bool DeleteNextCircularDNode(DLinkList L, DNode *p, int &e) {
    if (p == nullptr || p->next == L)
        return false;

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

    p->next = q->next;
    q->next->prior = p;
    delete q;

    return true;
}

5. 从前向后遍历

cpp 复制代码
void PrintCircularDList(DLinkList L) {
    DNode *p = L->next;

    while (p != L) {
        cout << p->data << ' ';
        p = p->next;
    }

    cout << '\n';
}

6. 从后向前遍历

cpp 复制代码
void PrintCircularDListReverse(DLinkList L) {
    DNode *p = L->prior;

    while (p != L) {
        cout << p->data << ' ';
        p = p->prior;
    }

    cout << '\n';
}

六、静态链表

静态链表使用数组下标代替指针。

1. 定义

cpp 复制代码
const int SMaxSize = 100;

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

typedef SNode SLinkList[SMaxSize];

其中:

  • data:数据域;
  • next:下一个结点的数组下标,也称游标;
  • -1:表示链表结束。

2. 遍历静态链表

假设 head 保存第一个数据结点的下标。

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

    while (p != -1) {
        cout << L[p].data << ' ';
        p = L[p].next;
    }

    cout << '\n';
}

3. 在结点 p 后插入下标为 s 的结点

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

4. 删除 p 的后继结点

cpp 复制代码
int DeleteNextSNode(SLinkList L, int p) {
    int q = L[p].next;

    if (q == -1)
        return -1;

    L[p].next = L[q].next;
    return q;  // 返回被删除结点的下标,之后可回收该空间
}

静态链表插入、删除时不移动元素,但需要自己维护哪些数组位置空闲。


七、线性表高频算法题模板

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

方法:保留不等于 x 的元素

cpp 复制代码
void DeleteX(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) \\

核心思想:

k 表示目前保留下来的元素个数,也表示下一个保留元素应写入的位置。


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

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

时间复杂度:

\O(n) \\

空间复杂度:

\O(1) \\


3. 逆置顺序表

cpp 复制代码
void Reverse(SqList &L) {
    for (int i = 0; i < L.length / 2; i++)
        swap(L.data[i], L.data[L.length - 1 - i]);
}

时间复杂度:

\O(n) \\

空间复杂度:

\O(1) \\


4. 合并两个递增顺序表

cpp 复制代码
bool Merge(SqList A, SqList B, SqList &C) {
    if (A.length + B.length > MaxSize)
        return false;

    int i = 0;
    int j = 0;
    int 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) \\

空间复杂度:

\O(m+n) \\


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

cpp 复制代码
void DeleteX(LinkList &L, int x) {
    LNode *p = L;

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

时间复杂度:

\O(n) \\

空间复杂度:

\O(1) \\

易错点:删除结点后,p 暂时不能后移。

原因是新的 p->next 仍然可能等于 x


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

cpp 复制代码
void ReverseByHeadInsert(LinkList &L) {
    LNode *p = L->next;
    L->next = nullptr;

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

        p->next = L->next;
        L->next = p;

        p = q;
    }
}

时间复杂度:

\O(n) \\

空间复杂度:

\O(1) \\

每轮操作:

  1. q 保存原链表剩余部分;
  2. p 插到头结点之后;
  3. p=q,继续处理原链表。

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

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

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

    L->next = pre;
}

三个指针的含义:

指针 含义
pre 已经逆置部分的表头
cur 当前正在处理的结点
next 尚未处理部分的表头

8. 查找单链表倒数第 k 个结点

cpp 复制代码
LNode *FindKthFromEnd(LinkList L, int k) {
    if (k <= 0)
        return nullptr;

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

    for (int i = 0; i < k; i++) {
        if (fast == nullptr)
            return nullptr;

        fast = fast->next;
    }

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

    return slow;
}

时间复杂度:

\O(n) \\

空间复杂度:

\O(1) \\

核心思想:始终让 fastslow 领先 k 个结点。


9. 查找单链表的中间结点

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

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

    return slow;
}

规则:

  • slow 每次走一步;
  • fast 每次走两步;
  • fast 到达末尾时,slow 到达中间。

当结点数为偶数时,上述代码返回两个中间结点中的后一个。


10. 判断单链表是否存在环

cpp 复制代码
bool HasCycle(LinkList L) {
    LNode *slow = L->next;
    LNode *fast = L->next;

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

        if (slow == fast)
            return true;
    }

    return false;
}

时间复杂度:

\O(n) \\

空间复杂度:

\O(1) \\


11. 寻找环的入口结点

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

    do {
        if (fast == nullptr || fast->next == nullptr)
            return nullptr;

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

    LNode *p = L->next;

    while (p != slow) {
        p = p->next;
        slow = slow->next;
    }

    return p;
}

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

复用原链表结点,不额外申请数据结点。

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

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

    tail->next = (pa != nullptr) ? pa : pb;

    delete B;
    B = nullptr;

    return A;
}

时间复杂度:

\O(m+n) \\

额外空间复杂度:

\O(1) \\


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

每次取出较小结点,再用头插法插入结果链表。

cpp 复制代码
LinkList MergeDecreasing(LinkList &A, LinkList &B) {
    LNode *pa = A->next;
    LNode *pb = B->next;
    A->next = nullptr;

    while (pa != nullptr && pb != nullptr) {
        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 != nullptr) ? pa : pb;

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

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

    delete B;
    B = nullptr;

    return A;
}

时间复杂度:

\O(m+n) \\

额外空间复杂度:

\O(1) \\


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

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

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

时间复杂度:

\O(n) \\


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

设原表为:

text 复制代码
a1 → a2 → a3 → a4 → a5 → a6

拆分后:

text 复制代码
A:a1 → a3 → a5
B:a2 → a4 → a6
cpp 复制代码
void SplitByPosition(LinkList &A, LinkList &B) {
    B = new LNode;
    B->next = nullptr;

    LNode *ra = A;
    LNode *rb = B;
    LNode *p = A->next;
    int position = 1;

    A->next = nullptr;

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

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

        position++;
        p = next;
    }
}

时间复杂度:

\O(n) \\

额外空间复杂度:

\O(1) \\


八、复杂度速查表

1. 基本操作复杂度

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

说明:

  1. 顺序表可直接通过数组下标访问,因此按位访问为 \(O(1)\)。
  2. 链表按位操作前通常需要先查找相应结点,因此总体为 \(O(n)\)。
  3. 单链表已知待删除结点时,通常还要找前驱;如果允许复制后继数据且不是尾结点,可以特殊地在 \(O(1)\) 内删除。
  4. 双链表已知待删除结点后,可以通过 prior 直接找到前驱。

2. 高频算法复杂度

算法 时间复杂度 额外空间复杂度
删除顺序表中所有 x \(O(n)\) \(O(1)\)
顺序表去重 \(O(n)\) \(O(1)\)
顺序表逆置 \(O(n)\) \(O(1)\)
合并两个有序顺序表 \(O(m+n)\) \(O(m+n)\)
删除链表中所有 x \(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)\)

九、常用指针操作速记

1. 单链表后插

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

口诀:

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


2. 单链表删除后继

cpp 复制代码
q = p->next;
p->next = q->next;
delete q;

口诀:

先保存被删结点,再跨过它,最后释放。


3. 双链表后插

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

口诀:

先连接新结点与后继,再连接新结点与前驱。


4. 双链表删除后继

cpp 复制代码
q = p->next;
p->next = q->next;
q->next->prior = p;
delete q;

5. 单链表逆置

cpp 复制代码
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;

口诀:

保存后继、反转指针、整体后移。


十、高频易错点

  1. 线性表是逻辑结构 ;顺序表和链表是存储结构

  2. 顺序表第 i 个元素对应:

    cpp 复制代码
    data[i - 1]
  3. 顺序表插入的合法位序为:

    \1\\le i\\le n+1 \\

  4. 顺序表删除的合法位序为:

    \1\\le i\\le n \\

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

  6. 链表本身不支持随机存取,按位查找为 \(O(n)\)。

  7. "链表插入、删除是 \(O(1)\)"必须有前提:已经找到相关结点。

  8. 带头结点单链表的空表条件:

    cpp 复制代码
    L->next == nullptr
  9. 不带头结点单链表的空表条件:

    cpp 复制代码
    L == nullptr
  10. 带头结点循环单链表的空表条件:

    cpp 复制代码
    L->next == L
  11. 普通单链表尾结点的判断条件:

    cpp 复制代码
    p->next == nullptr
  12. 循环单链表尾结点的判断条件:

    cpp 复制代码
    p->next == L
  13. 头插法建立的顺序与输入顺序相反;尾插法与输入顺序相同。

  14. 修改链表指针前,要先保存后继结点,否则可能断链。

  15. 删除连续多个满足条件的结点时,删除后前驱指针通常不能立即后移。

  16. 双链表插入、删除时,必须同时维护 priornext 两个方向。

  17. 遍历普通链表以 nullptr 为结束条件;遍历带头结点循环链表以重新到达头结点 L 为结束条件。