leetcode【数据结构简介】《链表》卡片——经典问题
文章目录
反转链表
如何反转一个单链表?
一种解决方案是按原始顺序迭代结点,并将它们逐个移动到列表的头部。
算法概述传送门
在该算法中,每个结点只移动一次。
因此,时间复杂度为 O(N),其中 N 是链表的长度。我们只使用常量级的额外空间,所以空间复杂度为 O(1)。
这个问题是你在面试中可能遇到的许多链表问题的基础。我们将在后面更多地讨论实现细节。
当然还有许多其他的解决方案。我们应该熟悉至少一个解决方案并能够实现它。
相关编程题
1. 反转链表
反转一个单链表。
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
【进阶】可以迭代或递归地反转链表。能否用两种方法解决这道题?
方法一:“换头术”
思路:
使用了上文所述的反转单链表的方法。我将之戏称为“换头术”:记录原本头(节点)的位置pos=head
,不停地将下面第一个备用的头pos->next
作为新的头放到现有头节点head
的上方(作为其前继),直到无头可换pos->next==NULL
,结束循环。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head){
if(head==NULL || head->next==NULL) return head;
struct ListNode* cur, * pos=head;
while(pos->next!=NULL){
cur = pos->next;
pos->next = cur->next;
cur->next = head;
head = cur;
}
return head;
}
方法二:迭代
思路:遍历链表时,将当前节点的next指针改为指向前一个节点原来是指向后一个节点)。
实现技巧:可以用两个变量,一个存储当前位置,一个存储当前位置的前继。因为如果不存储当前节点的前继,遍历到当前位置时,就找不到他的前继身在何方!这是由于单链表的性质决定的。最后返回新的头引用。
方法三:递归
比起迭代,递归就相当骚了~
单单使用文字,并不能很好理解递归算法。所以这里给出一个传送门,里面用文字+动态图的方式,清晰明了说明了递归的原理。(PS:里面的动态图是关键中的关键!建议配合代码里的注释一起食用~)
struct ListNode* reverseList(struct ListNode* head){
if(head==NULL || head->next==NULL) return head;
struct ListNode* cur = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return cur;
}
2. 移除链表元素
删除链表中等于给定值 val 的所有节点。
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val){
while(head!=NULL && head->val==val){
struct ListNode* cur=head;
head = cur->next;
free(cur);
}
if(head==NULL) return NULL;
if(head->next==NULL) return head;
struct ListNode* pos=head;
while(pos->next!=NULL){
if(pos->next->val==val){
struct ListNode* cur=pos->next;
pos->next = cur->next;
free(cur);
}
else pos = pos->next;
}
return head;
}
注意:在写链表的程序的时候,不要对空指针进行比较之类的操作。否则会报错如下:
Line 23: Char 21: runtime error: member access within null pointer of type 'struct ListNode' (solution.c)
切记!
3. 奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
【说明】应当保持奇数节点和偶数节点的相对顺序。
链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
滑动窗口法
思想:
一个快指针一个慢指针,快指针fast
在需要前移的节点的前继(就是要被前移节点的前一个节点)处停留,申请cur
(struct ListNode* cur;
)存储要被前移的节点,塞到慢指针slow
节点的后面。存和取的过程中需要完成链表的连接工作。
此算法使用原地算法完成。空间复杂度为O(1)。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* oddEvenList(struct ListNode* head){
if(head==NULL || head->next==NULL || head->next->next==NULL) return head;
struct ListNode* slow=head, * fast=head, * cur=head;
while(fast!=NULL && fast->next!=NULL && fast->next->next!=NULL){
fast = fast->next;
cur = fast->next;
fast->next = cur->next;
cur->next = slow->next;
slow->next = cur;
slow = slow->next;
}
return head;
}
辅助链表法
思路:将奇节点放在一个链表里,偶链表放在另一个链表里。然后把偶链表接在奇链表的尾部。
此法不如前面的方法,实现起来更加简单,也更加符合直觉思路,故不再赘述。
PS: 其实我想到的第一个方法就是滑动窗口法,虽然说有题目条件“原地算法”的干扰,但是我们也不应该忽略到辅助链表这种最基础的解法。切记!切记!
4. 回文链表
请判断一个链表是否为回文链表
输入: 1->2
输出: false
输入: 1->2->2->1
输出: true
法一:辅助数组+双指针
思路:
申请一个辅助数组存储链表中的所有节点的val值。
判断辅助数组是否为回文数组:使用头尾双指针,一个从头遍历,一个从尾遍历,进行比较判断。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool isPalindrome(struct ListNode* head){
if(head==NULL || head->next==NULL) return true; //特例判断
int ArraySize=0;
struct ListNode* pos=head;
while(pos->next!=NULL){
ArraySize++;
pos = pos->next;
}
ArraySize++;
int* array = (int *)malloc(sizeof(int) * ArraySize);
pos = head;
int i=0;
while(pos->next!=NULL){
array[i++] = pos->val;
pos = pos->next;
}
array[i] = pos->val;
int left=0, right=ArraySize-1;
while(left<right){
if(array[left]!=array[right]) break;
left++; right--;
}
free(array);
if(left==right || left==right+1) return true;
else return false;
}
【注意】:
动态数组,其创建麻烦,使用完必须由程序员自己通过free或者delete释放,否则严重会引起内存泄露。
静态数组在内存中位于栈区,是在定义时就已经在栈上分配了固定大小,在运行时这个大小不能改变,在函数执行完以后,系统自动销毁。free(array);
(PS:养成好习惯总是不会错~~)
【另外】至于使用动态数组或者是静态数组,就看你是要时间换空间
还是空间换时间
了~
从执行结果看,这程序算不得好,执行时间长,也多申请了一个O(n)的额外空间。那么有没有空间复杂度为O(1)的的解决方法呢?
法二:递归
法三:改变输入
思路:
反转后半部分链表(修改链表结构),然后用一个固定长度的滑动窗口来比较前半部分和后半部分。
要注意的是,比较完毕我们需要将链表回复原样。(这也算是一个好习惯吧。试想:我们写的只是一个函数,如果在主程序调用函数之后再次使用了该链表了呢?)
小结
【一些提示】
-
通过一些测试用例可以节省您的时间。
使用链表时不易调试。因此,在编写代码之前,自己尝试几个不同的示例来验证您的算法总是很有用的。 -
你可以同时使用多个指针。
有时,当你为链表问题设计算法时,可能需要同时跟踪多个结点。您应该记住需要跟踪哪些结点,并且可以*地使用几个不同的结点指针来同时跟踪这些结点。
如果你使用多个指针,最好为它们指定适当的名称,以防将来必须调试或检查代码。 -
在许多情况下,你需要跟踪当前结点的前一个结点。
你无法追溯单链表中的前一个结点。因此,您不仅要存储当前结点,还要存储前一个结点(前继)。这在双链表中是不同的,我们将在后面的章节中介绍。