跳到主要内容 数据结构初阶:顺序表、链表与时间空间复杂度 | 极客日志
C++ 算法
数据结构初阶:顺序表、链表与时间空间复杂度 介绍数据结构基础,涵盖时间空间复杂度计算的大 O 表示法,以及顺序表和链表的原理与实现。通过 C++ 代码演示了顺序表的动态扩容、增删查改,以及单链表的查找、插入、删除操作。此外,还分析了合并有序数组、删除链表节点、寻找中间节点、反转链表、检测环等经典算法问题的解决方案与代码实现。
云朵棉花糖 发布于 2026/3/22 更新于 2026/4/18 4 浏览时间复杂度和空间复杂度
理论部分
时间复杂度和空间复杂度的计算遵循大 O 表示法,通常按最坏情况计算。
大 O 表示法规则:
用常数 1 取代运行时间中的所有加法常数。
在修改后的运行次数函数中,只保留最高阶项。
如果最高阶项存在且不是 1,则去除与这个项目相乘的常数。得到的结果就是大 O 阶。
习题部分
注意循环执行次数,某些看似 n 次的循环实际未执行到 n 次。
题目:移除元素
原地移除,将不变元素移到前面。遇到不等于 val 的元素时,移到第 k 个位置即可。
int removeElement (vector<int >& nums, int val) {
int k = 0 ;
int n = nums.size ();
for (int i = 0 ; i < n; i++) {
if (nums[i] != val) nums[k++] = nums[i];
}
return k;
}
顺序表和链表
理论部分
链表能做的事,顺序表都可以完成,只是操作方法不同,效率不同。顺序表比链表唯一好的地方就是找下标为多少的位置好,其他都是链表好些。
顺序表跟数组非常相似,几乎可以说是一模一样。链表的话有用数组模拟的,还有用结构体去模拟的,一般来说,在竞赛方面,一般都是用数组去模拟,在工程方面的话,一般是动态去用结构体去模拟。
顺序表的动态模拟
typedef int SLDataType;
typedef struct SeqList {
SLDataType* a;
int size;
int capacity;
} SL;
void SLInit (SL* ps) {
assert (ps);
ps->a = (SLDataType*)malloc (sizeof (SLDataType)* INIT_CAPACITY);
if (ps->a == NULL ) {
perror ("malloc fail" );
;
}
ps->size = ;
ps->capacity = INIT_CAPACITY;
}
{
(ps);
(ps->a);
ps->a = ;
ps->capacity = ps->size = ;
}
{
(ps);
( i = ; i < ps->size; ++i) {
( , ps->a[i]);
}
( );
}
{
(ps);
(ps->size == ps->capacity) {
SLDataType* tmp = (SLDataType*) (ps->a, (SLDataType) * ps->capacity * );
(tmp == ) {
( );
;
}
ps->a = tmp;
ps->capacity *= ;
}
}
{
(ps);
(pos >= && pos <= ps->size);
(ps);
end = ps->size - ;
(end >= pos) {
ps->a[end + ] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
{
(ps);
(pos >= && pos < ps->size);
begin = pos + ;
(begin < ps->size) {
ps->a[begin - ] = ps->a[begin];
++begin;
}
ps->size--;
}
{
(ps);
( i = ; i < ps->size; ++i) {
(ps->a[i] == x) {
i;
}
}
;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
return
0
void SLDestroy (SL* ps)
assert
free
NULL
0
void SLPrint (SL* ps)
assert
for
int
0
printf
"%d "
printf
"\n"
void SLCheckCapacity (SL* ps)
assert
if
realloc
sizeof
2
if
NULL
perror
"realloc fail"
return
2
void SLInsert (SL* ps, int pos, SLDataType x)
assert
assert
0
SLCheckCapacity
int
1
while
1
void SLErase (SL* ps, int pos)
assert
assert
0
int
1
while
1
int SLFind (SL* ps, SLDataType x)
assert
for
int
0
if
return
return
-1
ps.a[ps.size++] 这里面的 ps.size 可以不打括号,因为是类似名字。
扩容一般喜欢扩为原来的二倍(因为以后 C++ 的 STL 中的一般都是这样搞的,比如 vector)。
开辟空间一定要检查开辟成功没有。说到开辟成功与否,还有个要注意的点是数组那个叫做越界,指针那个叫做空指针或者野指针,不要叫错了。
链表的动态模拟 typedef int SLTDataType;
typedef struct SListNode {
SLTDataType data;
struct SListNode * next;
} SLTNode;
SLTNode* SLTFind (SLTNode* phead, SLTDataType x) {
SLTNode* cur = phead;
while (cur) {
if (cur->data == x) {
return cur;
}
cur = cur->next;
}
return NULL ;
}
void SLTInsert (SLTNode** pphead, SLTNode* pos, SLTDataType x) {
assert (pos);
assert (pphead);
if (pos == *pphead) {
SLTPushFront (pphead, x);
} else {
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
SLTNode* newnode = BuySLTNode (x);
prev->next = newnode;
newnode->next = pos;
}
}
void SLTErase (SLTNode** pphead, SLTNode* pos) {
assert (pphead);
assert (pos);
if (*pphead == pos) {
SLTPopFront (pphead);
} else {
SLTNode* prev = *pphead;
while (prev->next != pos) {
prev = prev->next;
}
prev->next = pos->next;
free (pos);
}
}
void SLTInsertAfter (SLTNode* pos, SLTDataType x) {
assert (pos);
SLTNode* newnode = BuySLTNode (x);
newnode->next = pos->next;
pos->next = newnode;
}
void SLTEraseAfter (SLTNode* pos) {
assert (pos);
assert (pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free (del);
del = NULL ;
}
注意事项:其中有些函数的形参是 * 有些是 **,这个要看是要改什么。一般来说,传想改的东西的地址过去,比如你想改指针本身,那就要把指针的地址传过去。还有就是,链表的头节点要赋值给其他再拿去了,不然就会一去不复返。
常见算法题解析 链表这里最好单用头插或者尾插而不用中间插入删除这些(除非没有更好的思路了,不然麻烦)。取巧的办法是把链表里面的值放到数组中去搞(面试会卡空间复杂度让你不能用这个)。单链表的话一般不动头结点,而是再搞一个尾节点去动尾节点,不然后面要用头就不行了。单链表可能空的话建议加个哨兵位,不然要分很多类讨论。
创建结构体变量跟开辟空间的区别:创建结构体变量就相当于开辟了空间;创建的是结构体指针的话,要自己为结构体开辟空间。
struct Node * new_node = (struct Node*)malloc (sizeof (struct Node));
合并两个有序数组 从前面开始比较的话会需要把后面的往后推,从后面开始比较就不用,所以采用从后面开始比较的方法。
void merge (vector<int >& nums1, int m, vector<int >& nums2, int n) {
while (n > 0 && m > 0 ) {
if (nums1[m-1 ] > nums2[n-1 ]) {
nums1[m+n-1 ] = nums1[m-1 ];
m--;
} else {
nums1[m+n-1 ] = nums2[n-1 ];
n--;
}
}
while (n > 0 ) {
nums1[n-1 ] = nums2[n-1 ];
n--;
}
}
删除链表中等于给定值 val 的所有结点 传统做法:当前指针 cur,cur 前面的那个指针 prev,然后用 cur 去遍历看。不是 val 的值就尾插到新链表中。
struct ListNode * removeElements (struct ListNode* head, int val) {
struct ListNode * tail = NULL ;
struct ListNode * old = head;
int num = 0 ;
while (old) {
if (old->val != val) {
if (tail == NULL ) {
head = tail = old;
num++;
} else {
tail->next = old;
tail = tail->next;
num++;
}
}
old = old->next;
}
if (tail) {
tail->next = NULL ;
}
if (!num) {
head = NULL ;
}
return head;
}
链表的中间结点 做法:快慢指针。慢指针一次走一个 next,快指针一次走两个 next。
struct ListNode * middleNode (struct ListNode* head) {
struct ListNode *slow = head;
struct ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
反转链表 做法:取旧节点头插到新链表里。另外,这题让返回链表,其实就是返回头节点。
struct ListNode * reverseList (struct ListNode* head) {
struct ListNode * newhead = NULL ;
struct ListNode * a = head;
struct ListNode * next;
while (a) {
next = a->next;
a->next = newhead;
newhead = a;
a = next;
}
return newhead;
}
合并两个有序链表 做法:依次比较,取小的尾插。注意:头节点在第一次插的时候要记得给第一个插的节点的地址,而不是还是 eg:NULL。
struct ListNode * mergeTwoLists (struct ListNode* list1, struct ListNode* list2) {
if (list1 == 0 ) return list2;
if (list2 == 0 ) return list1;
struct ListNode * cur1 = list1, *cur2 = list2;
struct ListNode * list3 = NULL ;
struct ListNode * tail = NULL ;
while (cur1 && cur2) {
if (cur1->val > cur2->val) {
if (list3 == NULL ) {
list3 = tail = cur2;
cur2 = cur2->next;
} else {
tail->next = cur2;
cur2 = cur2->next;
tail = tail->next;
}
} else {
if (list3 == NULL ) {
list3 = tail = cur1;
cur1 = cur1->next;
} else {
tail->next = cur1;
cur1 = cur1->next;
tail = tail->next;
}
}
}
if (cur1 != NULL ) tail->next = cur1;
else tail->next = cur2;
return list3;
}
链表分割 做法:小于 x 的尾插到一个链表,大于等于的到尾插另一个链表。后面那个哨兵位的头结点不要。这里说不能改变原来数据的顺序,代码中间改没问题,只要最后跟原来一样就行。
class Partition {
public :
ListNode* partition (ListNode* pHead, int x) {
ListNode*ghead = NULL , *gtail = NULL , *lhead = NULL , *ltail = NULL ;
while (pHead) {
if (pHead->val < x) {
if (lhead == NULL ) {
lhead = ltail = pHead;
} else {
ltail->next = pHead;
ltail = ltail->next;
}
} else {
if (ghead == NULL ) {
ghead = gtail = pHead;
} else {
gtail->next = pHead;
gtail = gtail->next;
}
}
pHead = pHead->next;
}
if (gtail) gtail->next = NULL ;
if (ltail) ltail->next = ghead;
return lhead ? lhead : ghead;
}
};
链表的回文结构 做法:1. 找到中间节点。2. 从中间节点开始,对后半段逆置。3. 前半段和后半段比较就行。
bool chkPalindrome (ListNode* A) {
ListNode* slow = A, *fast = A;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
ListNode* cur = slow;
ListNode* prev = nullptr ;
ListNode* tmp = nullptr ;
while (cur) {
tmp = cur->next;
cur->next = prev;
prev = cur;
cur = tmp;
}
ListNode* head = prev;
while (head) {
if (head->val != A->val) return false ;
head = head->next;
A = A->next;
}
return true ;
}
相交链表 做法:1. 分别求两个链表的长度。2. 长的链表先走差距步。3. 再同时走,第一个地址相同的就是交点。
struct ListNode *getIntersectionNode (struct ListNode *headA, struct ListNode *headB) {
int lenA = 0 , lenB = 0 ;
struct ListNode * cur1 = headA;
struct ListNode * cur2 = headB;
while (cur1) { cur1 = cur1->next; lenA++; }
while (cur2) { cur2 = cur2->next; lenB++; }
int gap = abs (lenA - lenB);
struct ListNode * longlist = headA;
struct ListNode * shortlist = headB;
if (lenA < lenB) {
shortlist = headA;
longlist = headB;
}
while (gap--) longlist = longlist->next;
while (longlist) {
if (longlist == shortlist) return longlist;
longlist = longlist->next;
shortlist = shortlist->next;
}
return NULL ;
}
环形链表 做法:快慢指针(这里选择 fast 一次两步,slow 一次一步)。
延伸的常考问题:
为什么 slow 走 1 步,fast 走 2 步,他们会相遇?会不会错过?请证明。slow 进环以后,fast 开始追击 slow,slow 每走一步,fast 走两步,他们之间的距离缩小 1。当距离为 0 的时候就追上了。
为什么 slow 走 1 步,fast 走 n 步 (n>=3),他们会相遇?会不会错过?请证明。如果之间的距离 N 是 n-1 的倍数的话才能相遇,否则余数不为 0 是追不上的。
求环的长度:让 slow 再走到跟 fast 的相遇点的步数就是。
求入环的第一个结点(也就是入口点)。这里有个结论(带一个环的链表):一个指针从相遇点走,一个指针从起始点走,会在入口点相遇。
bool hasCycle (struct ListNode *head) {
struct ListNode * fast = head, *slow = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true ;
}
return false ;
}
复制带随机指针的链表 做法:1. 先复制旧节点然后连在每个旧节点后面(NULL 不复制)。2. 把新节点的 random 搞一下(新节点->random = 旧节点->random->next)。3. 把新节点从旧节点的链表上断下来成为新链表。
struct Node * copyRandomList (struct Node* head) {
struct Node * newhead = NULL ;
struct Node * new = NULL ;
struct Node * old = head;
struct Node * tail = NULL ;
while (old) {
new = (struct Node*)malloc (sizeof (struct Node));
new ->val = old->val;
new ->next = old->next;
tail = old->next;
old->next = new ;
old = tail;
}
old = head;
while (old) {
new = old->next;
if (old->random) new ->random = old->random->next;
else {
new ->random = NULL ;
}
if (old->next) old = old->next->next;
else {
break ;
}
}
old = head;
while (old) {
if (old == head) new = newhead = old->next;
if (old->next) old = old->next->next;
if (new ->next) {
new ->next = new ->next->next;
new = new ->next;
}
}
return newhead;
}