线性表常用操作与算法代码总结
适用范围:王道《数据结构》第二章线性表
代码语言: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) \\
每轮操作:
- 用
q保存原链表剩余部分; - 把
p插到头结点之后; - 令
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) \\
核心思想:始终让 fast 比 slow 领先 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)\) |
说明:
- 顺序表可直接通过数组下标访问,因此按位访问为 \(O(1)\)。
- 链表按位操作前通常需要先查找相应结点,因此总体为 \(O(n)\)。
- 单链表已知待删除结点时,通常还要找前驱;如果允许复制后继数据且不是尾结点,可以特殊地在 \(O(1)\) 内删除。
- 双链表已知待删除结点后,可以通过
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;
口诀:
保存后继、反转指针、整体后移。
十、高频易错点
-
线性表是逻辑结构 ;顺序表和链表是存储结构。
-
顺序表第
i个元素对应:cppdata[i - 1] -
顺序表插入的合法位序为:
\1\\le i\\le n+1 \\
-
顺序表删除的合法位序为:
\1\\le i\\le n \\
-
顺序表插入时从后向前移动,删除时从前向后移动。
-
链表本身不支持随机存取,按位查找为 \(O(n)\)。
-
"链表插入、删除是 \(O(1)\)"必须有前提:已经找到相关结点。
-
带头结点单链表的空表条件:
cppL->next == nullptr -
不带头结点单链表的空表条件:
cppL == nullptr -
带头结点循环单链表的空表条件:
cppL->next == L -
普通单链表尾结点的判断条件:
cppp->next == nullptr -
循环单链表尾结点的判断条件:
cppp->next == L -
头插法建立的顺序与输入顺序相反;尾插法与输入顺序相同。
-
修改链表指针前,要先保存后继结点,否则可能断链。
-
删除连续多个满足条件的结点时,删除后前驱指针通常不能立即后移。
-
双链表插入、删除时,必须同时维护
prior和next两个方向。 -
遍历普通链表以
nullptr为结束条件;遍历带头结点循环链表以重新到达头结点L为结束条件。