第二天——链表
其实链表这个东西,比较简单吧。一个数据域,一个指针域,常用的也就那么几种解决办法,双指针啥的。但是鉴于我是Java转C++,对我来说这个玩意的难点在于记得释放申请的空间。所以就练习一下并记录。
移除链表元素
这种题就没啥难度,唯一的问题在于删除第一个节点时需要返回新的头节点。一个合理的方法是设置一个虚拟头节点统一删除操作,这样最后只需返回虚拟头节点的指向节点就行。
代码如下:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* h = new ListNode(0); // 虚拟头节点
h->next = head;
ListNode* p = h;
while (p->next != NULL) {
if (p->next->val == val) {
ListNode* tmp = p->next;
p->next = p->next->next;
delete tmp; // 注意这里与Java的不同,需要读取删除节点手动删除
}
else
{
p = p->next;
}
}
head = h->next;
delete h; // 删除虚拟头节点
return head;
}
};
翻转链表
翻转链表确实是常规题目了属于是,搞三个节点分别记录当前节点、下一个节点以及前一个节点然后逐个翻转即可。
代码如下:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = NULL;
ListNode* tmp;
ListNode* cur = head;
while (cur)
{
tmp = cur->next; // 保存下一个节点
cur->next = pre; // 翻转
pre = cur;
cur = tmp;
}
return pre;
}
};
两两交换链表的节点
这题还是有点难度的,因为单链表需要保持前后连接需要记录,而两两交换决定了交换如果要完成需要中介。这时考虑添加一个虚拟头节点。以V->1->2->3->L为例说明我们的交换流程。先记录下需要拆开的节点V->next和V->next->next->next分别为t1和t2。
第一步 连接v和2
操作完的链表结构:V->2
此时记录t1为1
记录t2为3->L
第二步 连接2和t1
操作完的链表结构:V->2->1
此时还有记录t2:3->L
第三步 连接t1和t2
操作完的链表结构:V->2->1->3->L
然后我们将指向V的指针往后移动两项即可。
代码如下:
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* h = new ListNode(0); // 设置一个虚拟头结点
h->next = head;
ListNode* cur = h;
while (cur->next != NULL && cur->next->next != NULL) {
ListNode* t1 = cur->next; // 记录临时节点
ListNode* t2 = cur->next->next->next; // 记录临时节点
cur->next = cur->next->next; // 步骤一
cur->next->next = t1; // 步骤二
cur->next->next->next = t2; // 步骤三
cur = cur->next->next; // cur移动两位
}
return h->next;
}
};
删除链表的倒数第N个节点
简简单单的一道题,由于链表不像顺序表可以直接查位置,所以需要双指针法来找到位置。具体的操作就是快指针走N步然后快慢指针同时往后走,快指针走到头了直接删慢指针就行。这里注意如果N为表长需要处理头节点删除。
代码如下:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* fast = head;
ListNode* slow = head;
int k = 1, s = n; // 这里k记录表长,s用于暂存需要走的步数
if (head == nullptr)
{
return nullptr;
}
while (fast->next!=nullptr)
{
if (s == 0) // 快慢指针距离差为n时,两指针一起走
{
slow = slow->next;
fast = fast->next;
}
else // 否则就快指针往后走,记录走过的步长
{
fast = fast->next;
s--;
}
k++;
}
if (k == n) // 特殊处理删除第一个元素
{
ListNode* tmp = head->next;
delete head;
return tmp;
}
else
{
ListNode* tmp = slow->next;
slow->next = tmp->next;
delete tmp;
return head;
}
}
};
链表相交
如果两个链表相交,则必然从某一个节点开始到末尾有两表的指针相等。所以我们考虑先将两个链表末尾对其,然后求出两个链表的长度差值k,让长的链表指针先走k步,再同时移动,这样指针相等的节点就是交点了。
代码如下:
class Solution {
public:
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
ListNode* ap = headA;
ListNode* bp = headB;
int lenA = 0, lenB = 0;
int k = 0;
while (ap != nullptr) // A链表求长度
{
lenA++;
ap = ap->next;
}
while (bp != nullptr) // B链表求长度
{
lenB++;
bp = bp->next;
}
ap = headA;
bp = headB;
if (lenA > lenB) // 找到长的链表让他先走
{
k = lenA - lenB;
while (k != 0)
{
ap = ap->next;
k--;
}
while (ap != nullptr && bp != nullptr)
{
if (ap == bp)
{
return ap;
}
ap = ap->next;
bp = bp->next;
}
}
else
{
k = lenB - lenA;
while (k != 0)
{
bp = bp->next;
k--;
}
while (ap != nullptr && bp != nullptr)
{
if (ap == bp)
{
return ap;
}
ap = ap->next;
bp = bp->next;
}
}
return nullptr;
}
};
环形链表
这种题我是最不擅长的,虽然本质还是双指针,但是其实开始和结束条件没有那么直观,因此我们可以画图尝试去理解。
首先,怎么知道链表是否有环
由于双指针是快慢两个指针,不如我们考虑一条赛道上赛跑的两个人。如果一个人跑步速度始终比另一个人快,那么如果赛道是环形的,假以时日他就会追上慢的那个人。否则他们将不会再相遇。这时我们就知道了链表有环的判断条件:快慢指针在快指针到达出口前相遇。
然后是环的入口
老实讲,一开始我是真没啥头绪,但是如果你在纸上画出一个例子并且自己尝试模拟情况,你就会发现,由于快指针先进入了环,实际上就会在环内循环走并等待慢指针。这时慢指针再进入环,其相遇的节点距离环的入口一定是 环前长度%环的大小
这时我们再从相遇节点开始,一个指针设为链表头,一个指向相遇节点,两个都每次移动一步,再次相遇时我们就得到了环的入口。
代码如下:
class Solution {
public:
ListNode* detectCycle(ListNode* head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast != nullptr && fast->next != nullptr)
{
slow = slow->next; // 快走两步,慢走一步
fast = fast->next->next;
if (fast == slow) // 有环重设一个指针位置,然后两个都只走一步找入口
{
fast = head;
while (fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
}
return nullptr;
}
};