跳到主要内容 数据结构:顺序表与链表常用算法解析 | 极客日志
C 算法
数据结构:顺序表与链表常用算法解析 数据结构中顺序表和链表的经典算法题。涵盖双指针法在数组中的应用(如移除元素、去重、合并),以及快慢指针、三指针法在链表操作中的实现(如反转、找中间节点、判环、相交检测)。提供了详细的思路分析、时间复杂度说明及 C 语言代码示例,并包含哨兵位优化技巧。
CodeArtist 发布于 2026/3/24 更新于 2026/4/19 6 浏览顺序表算法题(双指针法)
移除元素
思路 :双指针法,创建两个变量 dst, src。如果 src 指向的数据是 val,src++;如果 src 指向的数据不是 val,赋值 nums[dst] = nums[src],然后 src 和 dst 都 ++。src 在前面探路(找非 val 值),dst 在后面站岗(保存非 val 值)。
时间复杂度 :O(N)
空间复杂度 :O(1)
int removeElement (int * nums, int numsSize, int val) {
int src = 0 , dst = 0 ;
while (src < numsSize) {
if (nums[src] != val) {
nums[dst] = nums[src];
dst++;
}
src++;
}
return dst;
}
删除有序数组中的重复项
思路 :双指针法,创建两个变量,分别指向数组起始和下一个位置。如果 src 的值和 dst 的值相等,src++;如果 src 的值和 dst 的值不相等,dst++,src 赋值给 dst,src++。
时间复杂度 :O(N)
空间复杂度 :O(1)
int removeDuplicates (int * nums, int numsSize) {
int src = 1 , dst = ;
(src < numsSize) {
(nums[src] != nums[dst]) {
dst++;
nums[dst] = nums[src];
}
src++;
}
dst + ;
}
{
src = , dst = ;
(src < numsSize) {
(nums[src] != nums[dst] && ++dst != src) {
nums[dst] = nums[src];
}
src++;
}
dst + ;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 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
0
while
if
return
1
int
removeDuplicatesOptimized
(int * nums, int numsSize)
int
1
0
while
if
return
1
合并两个有序数组
void merge (int * nums1, int nums1Size, int m, int * nums2, int nums2Size, int n) {
int l1 = m - 1 , l2 = n - 1 , l3 = m + n - 1 ;
while (l1 >= 0 && l2 >= 0 ) {
if (nums1[l1] > nums2[l2]) {
nums1[l3--] = nums1[l1--];
} else {
nums1[l3--] = nums2[l2--];
}
}
while (l2 >= 0 ) {
nums1[l3--] = nums2[l2--];
}
}
链表算法题(快慢指针,三指针法,创建新链表法)
移除链表元素 思路 :创建新链表,将原链表中不为 val 的节点拿下来尾插。
注意 :此处创建新链表并非真创建新链表,而是通过修改原本的链表。
typedef struct ListNode ListNode ;
struct ListNode* removeElements (struct ListNode* head, int val) {
ListNode* newHead, *newTail;
newHead = newTail = NULL ;
ListNode* pcur = head;
while (pcur) {
if (pcur->val != val) {
if (newHead == NULL ) {
newHead = newTail = pcur;
} else {
newTail->next = pcur;
newTail = pcur;
}
}
pcur = pcur->next;
}
if (newTail) {
newTail->next = NULL ;
}
return newHead;
}
反转链表 思路 1 :创建新链表,遍历原链表将节点头插到新链表中。
注意 :此处创建新链表并非真创建新链表,通过不断拆解这两个链表合为一个,因此要格外注意保存下一个节点和尾节点的 next 为 NULL。
时间复杂度 :O(N)
空间复杂度 :O(1)
思路 2 :创建三个指针,改变指针的指向。
时间复杂度 :O(N)
空间复杂度 :O(1)
typedef struct ListNode ListNode ;
struct ListNode* reverseList (struct ListNode* head) {
ListNode* pcur = head;
ListNode* newHead = head;
while (pcur) {
ListNode* next = pcur->next;
pcur->next = newHead;
newHead = pcur;
pcur = next;
}
if (head)
{
head->next = NULL ;
}
return newHead;
}
typedef struct ListNode ListNode ;
struct ListNode* reverseListTwo (struct ListNode* head) {
if (head == NULL )
{
return NULL ;
}
ListNode* n1, *n2, *n3;
n1 = NULL , n2 = head, n3 = n2->next;
while (n2) {
n2->next = n1;
n1 = n2;
n2 = n3;
if (n3) {
n3 = n3->next;
}
}
return n1;
}
链表的中间节点 思路 :快慢指针,慢指针每次走一步,快指针每次走两步。
typedef struct ListNode ListNode ;
struct ListNode* middleNode (struct ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL )
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
合并两个有序链表 思路 :创建新链表,遍历并比较原链表中节点的值,小的尾插到新链表中。
注意 :此处创建新链表并非真创建新链表,通过不断拆解这两个链表合为一个,因此要格外注意保存下一个节点和尾节点的 next 为 NULL。
typedef struct ListNode ListNode ;
struct ListNode* mergeTwoLists (struct ListNode* list1, struct ListNode* list2) {
if (list1 == NULL ) {
return list2;
}
if (list2 == NULL ) {
return list1;
}
ListNode* newHead, *newTail;
newHead = newTail = NULL ;
ListNode* l1 = list1;
ListNode* l2 = list2;
while (l1 != NULL && l2 != NULL ) {
if (l1->val < l2->val) {
if (newHead == NULL ) {
newHead = newTail = l1;
} else {
newTail->next = l1;
newTail = newTail->next;
}
l1 = l1->next;
} else {
if (newHead == NULL ) {
newHead = newTail = l2;
} else {
newTail->next = l2;
newTail = newTail->next;
}
l2 = l2->next;
}
}
if (l1) {
newTail->next = l1;
}
if (l2) {
newTail->next = l2;
}
return newHead;
}
以上的代码有些冗余(if 判断 NULL 太多),如果创建一个哨兵位可以更加简洁
typedef struct ListNode ListNode ;
struct ListNode* mergeTwoListsOptimized (struct ListNode* list1, struct ListNode* list2) {
if (list1 == NULL ) {
return list2;
}
if (list2 == NULL ) {
return list1;
}
ListNode* newHead, *newTail;
newHead = newTail = (ListNode*)malloc (sizeof (ListNode));
ListNode* l1 = list1;
ListNode* l2 = list2;
while (l1 != NULL && l2 != NULL ) {
if (l1->val < l2->val) {
newTail->next = l1;
newTail = newTail->next;
l1 = l1->next;
} else {
newTail->next = l2;
newTail = newTail->next;
l2 = l2->next;
}
}
if (l1) {
newTail->next = l1;
}
if (l2) {
newTail->next = l2;
}
ListNode* retHead = newHead->next;
free (newHead);
newHead = NULL ;
return retHead;
}
链表分割 思路 :创建两个链表 (小链表、大链表),遍历原链表,小的尾插到小链表中,大的尾插到大链表中,大链表和小链表首尾相连。
注意 :此处创建新链表并非真创建新链表,通过不断拆解这两个链表合为一个,因此要格外注意保存下一个节点和尾节点的 next 为 NULL。
class Partition {
public:
ListNode* partition (ListNode* pHead, int x) {
ListNode* lessHead, *lessTail;
lessHead = lessTail = (ListNode*)malloc (sizeof (ListNode));
ListNode* greatHead, *greatTail;
greatHead = greatTail = (ListNode*)malloc (sizeof (ListNode));
ListNode* pcur = pHead;
while (pcur) {
if (pcur->val < x) {
lessTail->next = pcur;
lessTail = lessTail->next;
} else {
greatTail->next = pcur;
greatTail = greatTail->next;
}
pcur = pcur->next;
}
greatTail->next = NULL ;
lessTail->next = greatHead->next;
ListNode* ret = lessHead->next;
free (lessHead);
free (greatHead);
return ret;
}
};
链表的回文结构 思路 1 :创建数组 (大小 900),遍历链表将节点的值依次存储在数组中,若数组为回文结构,则链表为回文结构。
思路 2 :找链表中间节点,将中间节点作为新链表的头节点,反转链表遍历原链表和反转后链表节点的值是否相等。
中间节点 -> 快慢指针
反转链表 -> 三指针法
bool chkPalindrome (ListNode* A) {
int arr[900 ] = {0 };
ListNode* pcur = A;
int i = 0 ;
while (pcur) {
arr[i++] = pcur->val;
pcur = pcur->next;
}
int left = 0 , right = i - 1 ;
while (left < right) {
if (arr[left] != arr[right]) return false ;
left++;
right--;
}
return true ;
}
class PalindromeList {
public:
struct ListNode* middleNode (struct ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL )
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
struct ListNode* reverseList (struct ListNode* head) {
if (head == NULL )
{
return NULL ;
}
ListNode* n1, *n2, *n3;
n1 = NULL , n2 = head, n3 = n2->next;
while (n2) {
n2->next = n1;
n1 = n2;
n2 = n3;
if (n3) {
n3 = n3->next;
}
}
return n1;
}
bool chkPalindrome (ListNode* A) {
ListNode* mid = middleNode(A);
ListNode* right = reverseList(mid);
ListNode* left = A;
while (right) {
if (left->val != right->val) return false ;
left = left->next;
right = right->next;
}
return true ;
}
};
相交链表 思路 :求两个链表的长度,长链表先走长度差步,长短链表开始同步遍历,找相同的节点。两个链表的尾节点相同,两个链表一定相交。
typedef struct ListNode ListNode ;
struct ListNode* getIntersectionNode (struct ListNode* headA, struct ListNode* headB) {
ListNode* pa = headA;
ListNode* pb = headB;
int sizeA = 0 , sizeB = 0 ;
while (pa) {
++sizeA;
pa = pa->next;
}
while (pb) {
++sizeB;
pb = pb->next;
}
int gap = abs (sizeA - sizeB);
ListNode* shortList = headA;
ListNode* longList = headB;
if (sizeA > sizeB) {
longList = headA;
shortList = headB;
}
while (gap--) {
longList = longList->next;
}
while (shortList)
{
if (shortList == longList) {
return shortList;
}
shortList = shortList->next;
longList = longList->next;
}
return NULL ;
}
环形链表(快慢指针)
环形链表 I 思路 :快慢指针,慢指针每次走一步,快指针每次走两步。如果 slow 和 fast 指向同一个节点,说明链表带环。
typedef struct ListNode ListNode ;
bool hasCycle (struct ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
return true ;
}
}
return false ;
}
思考 1 :为什么快指针每次走两步,慢指针走一步可以相遇,有没有可能遇不上,请推理证明!
slow 一次走一步,fast 一次走 2 步,fast 先进环,假设 slow 也走完入环前的距离,准备进环,此时 fast 和 slow 之间的距离为 N,接下来的追逐过程中,每追击一次,他们之间的距离缩小 1 步。
因此,在带环链表中慢指针走一步,快指针走两步最终一定会相遇。
思考 2 :快指针一次走 3 步,走 4 步,…n 步行吗?
分析 :
如果 N 是偶数,第一轮就追上了。
如果 N 是奇数,第一轮追不上,快追上,错过了,距离变成 -1,即 C-1,进入新的一轮追击。
a. C-1 如果是偶数,那么下一轮就追上了。
b. C-1 如果是奇数,那么就永远都追不上。
总结一下追不上的前提条件:N 是奇数,C 是偶数 。
step2 : 假设环的周长为 C,头结点到 slow 结点的长度为 L,slow 走一步,fast 走三步,当 slow 指针入环后,slow 和 fast 指针在环中开始进行追逐,假设此时 fast 指针已经绕环 x 周。
在追逐过程中,快慢指针相遇时所走的路径长度:
• fast:L + xC + C - N
• slow:L
由于慢指针走一步,快指针要走三步,因此得出:3 * {慢指针路程} = {快指针路程},即:
3L = L + xC + C - N
化简得:2L = (x + 1)C - N
对上述公式继续分析:由于偶数乘以任何数都为偶数,因此 2L 一定为偶数,则可推导出可能的情况:
• 情况 1:偶数 = 偶数 - 偶数
• 情况 2:偶数 = 奇数 - 奇数
由 step1 中(1)得出的结论,如果 N 是偶数,则第一圈快慢指针就相遇了。由 step1 中(2)得出的结论,如果 N 是奇数,则 fast 指针和 slow 指针在第一轮的时候套圈了,开始进行下一轮的追逐;当 N 是奇数,要满足以上的公式,则 (x+1)C 必须也要为奇数,即 C 为奇数,满足(2)a 中的结论,则快慢指针会相遇。因此,step1 中的 N 是奇数,C 是偶数不成立,既然不存在该情况,则快指针一次走 3 步最终一定也可以相遇。快指针一次走 4、5……步最终也会相遇,其证明方式同上。
typedef struct ListNode ListNode ;
bool hasCycleThreeSteps (struct ListNode* head) {
ListNode* slow, *fast;
slow = fast = head;
while (fast && fast->next) {
slow = slow->next;
int n = 3 ;
while (n--) {
if (fast->next) fast = fast->next;
else return false ;
}
if (slow == fast) {
return true ;
}
}
return false ;
}
环形链表 II 思路 :快慢指针,在环里一定会相遇。相遇点到入环节点的距离 == 头结点到入环节点的距离。
typedef struct ListNode ListNode ;
struct ListNode* detectCycle (struct ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
ListNode* pcur = head;
while (pcur != slow) {
pcur = pcur->next;
slow = slow->next;
}
return pcur;
}
}
return NULL ;
}
思考 :为什么相遇点到入环节点的距离 == 头结点到入环节点的距离
说明 :
H 为链表的起始点,E 为环入口点,M 为判环时的相遇点
设:
环的长度为 R,H 到 E 的距离为 L,E 到 M 的距离为 X,则:M 到 E 的距离为 R-X
在判环时,快慢指针相遇时所走的路径长度:
• fast:L + X + nR
• slow:L + X
注意:当慢指针进入环时,快指针可能已经在环中绕了 n 圈了,n 至少为 1
因为:快指针先进环走到 M 的位置,最后又在 M 的位置与慢指针相遇
慢指针进环之后,快指针肯定会在慢指针走一圈之内追上慢指针
因为:慢指针进环后,快慢指针之间的距离最多就是环的长度,而两个指针在移动时,每次它们之间的距离都缩减一步,因此在慢指针移动一圈之前快指针肯定是可以追上慢指针的,而快指针速度是慢指针的两倍,因此有如下关系:
2 * (L+X) = L+X + nR
化简得:
L+X = nR
L = nR - X
进一步变形:
L = (n-1)R + (R-X)
(n 为 1,2,3,4,……,n 的大小取决于环的大小,环越小 n 越大)
极端情况下,假设 n=1,此时:
L = R - X