今天主要练习链表相关的题,练习时特别重要的的一个点在于画图,画图是最容易让人理解链表操作的方式。笔者在最初学习C++时观看的网课,当时的老师就是通过画图的方式非常清晰地讲解了链表、队列等的各种操作,为自己如今的理解奠定了良好的基础。
707.设计链表
题目:设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3
思路:
1.这一题是很经典的初学数据结构便会接触的,手敲链表实现各种操作。
2.引入虚拟头结点,这样做的好处是对于涉及操作链表头结点的时候更加方便。
class MyLinkedList {
public:
struct ListNode{
int val;
ListNode* next;
ListNode(int val): val(val),next(nullptr){}
};
MyLinkedList(){
preHead = new ListNode(0);
size = 0;
}
int get(int index){
if(index < 0 || index > size - 1){
return -1;
}
ListNode* cur = preHead;
while(index--){
cur = cur->next;
}
return cur->next->val;
}
void addAtTail(int val){
ListNode* cur = preHead;
while(cur->next){
cur = cur->next;
}
ListNode* newNode = new ListNode(val);
cur->next = newNode;
size++;
}
void addAtHead(int val){
ListNode* newNode = new ListNode(val);
newNode->next = preHead->next;
preHead->next = newNode;
size++;
}
void addAtIndex(int index,int val){
if(index == size) addAtTail(val);
else if(index > size) return;
else if(index < 0) addAtHead(val);
else{
ListNode* cur = preHead;
while(index--){
cur = cur->next;
}
ListNode* newNode = new ListNode(val);
newNode->next = cur->next;
cur->next = newNode;
size++;
}
}
void deleteAtIndex(int index){
if(index < 0 || index > size - 1){
return;
}
ListNode* cur = preHead;
while(index--){
cur = cur->next;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
size--;
}
private:
int size;
ListNode* preHead;
};
启发:
1.虚拟头结点在大多数链表相关题目中都能有很好的作用,让我们不用单独去对头结点进行操作。
2.在单向链表中,要想对某一个结点进行删除、插入之类的操作,一定得先移动到它的前一个结点而不能移动到它本身。
24.两两交换链表中的结点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路:
1.交换链表结点实际上就是改变next指向的连线,而在本题中实际改变后的连接稍显混乱,所以不熟练的情况下一定要画图方便理解。
2.可能在交换的一开始并不知道要临时存储哪些结点,所以在写交换的过程时多思考,发现需要临时存储时就补上。通过画图观察全部过程后可以看出,首先需要存储要交换的两个结点的第一个结点(第二步到第三步),再保存两个结点的第二个结点的之后一个结点(第三步到第四步)。
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* preHead = new ListNode(0);
if(head == nullptr || head->next == nullptr){
return head;
}
preHead->next = head;
ListNode* cur = preHead;
while(cur->next != nullptr && cur->next->next != nullptr){
ListNode* tmp = cur->next;
ListNode* tmp1 = cur->next->next->next;
cur->next = cur->next->next;
cur->next->next = tmp;
cur->next->next->next = tmp1;
cur = cur->next->next;
}
return preHead->next;
}
};
启发:
1.交换结点说简单不简单说困难不困难,一定多画图了解整个过程
19.删除链表的倒数第n个结点
题目:给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
思路:
1.在涉及倒数第n个这样的情况时,一般都会采用快慢指针,让快指针先走n步,然后二者再一起走,这样慢指针最后指向的就是倒数第n个元素。但需要注意的是,如果要删除第n个结点,慢指针最后必须指向的是第n-1个结点!
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* preHead = new ListNode(0);
preHead->next = head;
ListNode* fast = preHead;
ListNode* slow = preHead;
n++;//这里是为了让快指针在最初多移动一位,这样二者同时移动时就会少移动一位,让慢指针最终指向第n-1个结点
while(n-- && fast!=nullptr){
fast = fast->next;
}
while(fast != nullptr){
fast = fast->next;
slow = slow->next;
}
ListNode* tmp = slow->next;
slow ->next = slow->next->next;
delete tmp;
return preHead->next;
}
};
启发:
1.涉及倒数第n个情况,首先考虑快慢指针!
面试题02.07 链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
C++快慢指针法
思路:
1.需要注意要找的是相交的结点,并不是说结点值相同就一定是相交结点。
2.想要的效果肯定是让两个指针分别指向两个链表,并且同时指向相交的结点。为了达到这个目的我们可以求出两条链表的长度,求出二者长度之差,然后让较长链表的结点先走长度的差值,这样两个指针就做到了站在同一起跑线的效果。本题中为了方便,一直让A链表作为较长的链表。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while(curA!=NULL){
curA = curA->next;
lenA++;
}
while(curB!=NULL){
curB = curB->next;
lenB++;
}
curA = headA;
curB = headB;
if(lenB >lenA){
swap(curA,curB);
swap(lenA,lenB);
}
int gap = lenA - lenB;
while(gap--){
curA = curA->next;
}
while(curA!=NULL){
if(curA == curB){
return curA;
}
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
C#字典法
思路:
1.另一种思路是,借助哈希表来存储每一个结点及其对应的值,让结点值作为键,让结点作为值。每遇到一个哈希表中未有的键时就将该节点存储进入哈希表,在遇到哈希表中已有的键时,说明当前结点的值在之前已经被记录过,此时再比较该键所对应的值看二者是否是同一节点。不过笔者在写这道题时对哈希表仍不熟练,所以采用的C#字典来完成同样的操作。
public class Solution {
public ListNode GetIntersectionNode(ListNode headA, ListNode headB) {
Dictionary<int,ListNode> dict = new Dictionary<int,ListNode>();
ListNode tmpA = headA;
ListNode tmpB = headB;
while(tmpA!=null || tmpB != null){
if(tmpA!=null){
if(!dict.ContainsKey(tmpA.val)){
dict.Add(tmpA.val,tmpA);
tmpA = tmpA.next;
}
else{
if(dict[tmpA.val] == tmpA){
return tmpA;
}
else{
tmpA = tmpA.next;
}
}
}
if(tmpB!=null){
if(!dict.ContainsKey(tmpB.val)){
dict.Add(tmpB.val,tmpB);
tmpB = tmpB.next;
}
else{
if(dict[tmpB.val] == tmpB){
return tmpB;
}
else{
tmpB = tmpB.next;
}
}
}
}
return null;
}
}
启发:
1.对于判断两个链表是否相交,如何找到相交的结点非常重要。本题中两个方法分别对应两种思路:一种是让遍历两个链表的指针站在同一起跑线上;一种是存储已遍历过的结点并与当前结点进行比较。
142.环形链表
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思路:
1.本题主要解决两个问题:(1)如何判断链表有环?(2)如何找到环入口的结点索引?
2.对于问题一,可以用快慢指针的方法。快指针每次走两格,慢指针每次走一格,如果存在环,二者一定会在环内相遇。
3.对于问题二,需要通过一定的数学推理。注意我们要求的是x
我们假设两个指针相遇时,慢指针走了x+y的节点数,快指针则走了x + y + n(y + z)节点数,因为快指针走的结点个数一定是慢指针的两倍,所以可以列出方程2(x+y) = x + y + n(y + z)。经过一系列化简后可以得到x = (n-1)(y + z) + z,其中当n=1时,x = z。这样就代表,当两个指针一个index1从头结点出发每次走一结点,一个index2从快慢指针相遇的结点出发每次走一个结点,二者相遇的地方即为环入口结点。如果取n>1,依然可以用该法,此时只是说fast指针在环内转了n圈后才与slow相遇,而index2在环内多走了n-1圈才和index1相遇,实际效果一致。
3.为什么慢指针一定是走了x+y个结点数呢?慢指针有没有可能在环内先走了m圈后再和快指针相遇?显然是不可能的,当慢指针进入环内时,一定是快指针追慢指针,因为快指针每次比慢指针多走一个结点,所以快指针最后一定会和慢指针相遇,且一定在慢指针走完一圈前与其相遇。如果从物理的角度来说,快指针对于慢指针的相对速度为1,如果慢指针已经走完了一圈,那么快指针一定走了一圈多,超过了慢指针。但是快指针不可能错过慢指针,所以一定在慢指针走完一圈前相遇。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast!=NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
ListNode* index1 = head;
ListNode* index2 = fast;
while(index1 != index2){
index1 = index1->next;
index2 = index2->next;
}
return index2;
}
}
return NULL;
}
};
启发:
1.本题第一次将数学逻辑推理引入算法题中,一定要学会拆解问题,本题的核心两大问题搞清楚后就可以一个一个进行解决。
2.关于第二个问题的逻辑推理,结合物理运动学的角度进行思考就会发现快慢指针相遇的特点。