刷题时间: 2019/04/20 –
主播:yxc(闫雪灿)
视频链接: https://v.douyu.com/show/jwzOvpw6DDEWZVRm
解题心得
No.147 对链表插入排序 (AC)(划重点)
- 本题的思路是:从前往后,找到第一个比当前结点大的结点,然后将当前结点插入到它的前面。
- 本比很容易写出Bug. 第一个需要注意的点是,按照自己的惯常思路和想法,容易出现尾指针没有指向空的问题。 按照yxc的思路写时, 需要特别注意,新建立的虚拟头结点不指向head指针,否者也是Bug,这个可以举一个例子画画图理解。
No.138 复杂链表的赋值 (AC)
- step1: 在链表每个节点后插入一个它的新建赋值结点;
- step2: 给新插入的结点赋值random指针的值
- step3: 从修改链表中把赋值的链表拎出来。并将原链表结构还原。还原原链表非常关键,否则就不是deep copy了,之前程序不能ac就是这个原因。
- 还有一点是,要看结点的构造函数,在本题中,初始化时,next指针和random指针也需要给定值,否则会报指针错误。
No.86 将链表按照指定规则调整顺序 (AC)
- 插入位置可能是头结点之前,所以需要建立一个虚拟头结点;
- 先找到第一个值比target小的结点p,作为插入结点的头,然后新建一个q结点从p之后遍历,并记录他的前驱结点和后继结点。当q结点的值小于target时,将q结点插入到p之后,为了保持位置关系,p结点后移。同时q结点这边的连接关系也需要更新。
No.30 Substring with Concatenation of All Words (AC)
- 枚举的方法,时间复杂度为O(n* len),循环n次,每次只要判断word_num * word_len的长度即可。
No.24. 成对交换前后两个结点对 (AC)
- 需要注意的是,pre->1->2->3中,把1指向3, pre指向2之后,好需要将2指向1,不然中间就断了。
No.19 移除链表中的倒数第n和结点 (AC)
- 链表问题通常存在边界问题,如头结点会被删除,解决这个问题通常是定义一个虚拟头结点,指向真正的头结点。
- 在本题中,用双指针找到倒数第n个结点,然后删除。整个过程只要扫描一遍。
- 还有一个边界是删除倒数第0个结点,即不删除,这种情况直接返回即可。
No.83 删除排序链表中的重复元素 (AC)
- 从前往后遍历,若前后两个元素相等,则删除后者,否者继续往后扫描。
No.206 翻转链表 (AC)
- 需要记录一个前驱结点和一个后继结点。
- 最终前驱结点即为翻转链表的新结点。
No.92 翻转链表中指定段 (AC)
- 头结点也有可能被翻转,因此需要定义一个虚拟头结点。
- 先定位要翻转的段,翻转完成之后,再链接到原链表中。
- 最好不要用pre = pre->next; 去更新pre结点,因为链接顺序改变之后,直接这样做很可能会指向其他结点。
No.61 循环数组 (AC)
- 这道题有两种做法。
- 第一种方法是单个往前移,需要用一个deque队列存放链表中的元素。
- 由于k可能比链表长度大,所以被弹出的尾结点在处理完之后再添加到deque队列的对手。
- 有几个边界特判条件:当链表为空,或链表长度为1,或k为0时都可以直接返回。
- 第二种方法是整段往前移。这里一定要处理的是k大于链表长度的情况。k对链表长度取模,若为0,则直接返回,否则,移到链表前面。
- 这两种方法中头结点都可能会被翻转,所以在表头增加一个虚拟结点。
No.143 数字重排序 (AC) 划重点!
- 本题可以用一个栈辅助操作,需要额外O(n)的空间
- 更好的方法是,找到中点,将后半部分翻转,然后与前半部分合并。
- 几个实现细节:1. 将前后两小段的尾结点指向空,方便后边合并。2.合并时容易写错。
- 假设两个待合并链表为pNode和pPre,
若按照先1后2的方式合并(!错误!),代码如下:
for (int i = num + 1 >> 1; i < num; i++){
ListNode* pNext = pNode->next;
pNode->next = pPre;
pPre = pPre->next;
pPre->next = pNext; // ERROR! 此时pPre指向的位置已经别改变了
pNode = pNext;
}
正确的方式是先2后1,代码如下:
for (int i = num + 1 >> 1; i < num; i++){
ListNode* t = pPre->next;
pPre->next = pNode->next;
pNode->next = pPre;
pNode = pNode->next->next;
pPre = t;
}
No.141 判断链表是否有环 (AC)(易错题)
- 快慢指针来做
- 本题很容易写出bug,有两个地方值得注意一下:
bool hasCycle(ListNode *head) {
if (!head) return false;
ListNode* fast = head->next;
ListNode* slow = head;
while(fast != slow){
/*
// bug写法
// 若只有一个元素,那么指针始终无法后移,造成TLE
// 因此可以将快慢指针分开来后移,这样可以保证慢指针能往后移
if (fast && fast->next && slow) { // 不加fast,若只有一个元素,会溢出
fast = fast->next->next;
slow = slow->next;
}
*/
if (fast && fast->next ) { // 若只有一个元素,会溢出
fast = fast->next->next;
}
if (slow) slow = slow->next;
if(!fast || !slow) return false;
}
return !fast->next ? false : true; // 需要判断是否下一个指针为结尾
// 反例: 1,2 -1 这种情况下快慢指针仍可以相等,但并没有形成环,因此需要判断相遇位置是否是尾结点的前一个位置
}
NO.160 找两个链表的公共结点 (AC) (易错题)
- 基本思路:双指针把来哥哥链表走两遍,相遇的位置即为交点。
- 特判边界:1. 两个链表没有交点。这种情况下什么时候跳出循环以及返回什么值需要考虑一下,容易写出bug,详见代码。 2. 两个链表本身就是同一个链表,即从第一个元素起就相同,这种情况需要特判,否则会返回nullptr。
NO.142 寻找循环链表的环入口 (AC)
- Step1: 快慢指针找到换种的相遇点
- Step2: 计算环的大小k
- Step3: 双指针前后相隔k步出发,相遇点即为环的入口。
- 需要特判的情况: fast->next-next是否存在;环是否存在。若环不存在,则fast || fast->next || slow中一定有个指针会到达nullptr结点,此时直接返回nullptr即可。
No.109 将排序链表转换成平衡搜索二叉树 (AC)
- 思路类似于根据中序和先序重构二叉树,用递归的方法来做。
- 在写的时候需要注意满足搜索二叉树的需求,这里很巧,正常写就可以满足。
No.82 将链表中重复的数字全部删除 (AC)
- 需要记录一个前驱结点和一个cur结点,关键两行代码如下:
while(q){
while(q && p->next->val == q->val)
q = q->next; // 是判断结点的值相等,不是结点相等,因为结点还有next指针
if (p->next->next == q) p = p->next; // 此时是需要指向相同的结点
else p->next = q;
}
No. 148 链表排序(归并)(AC)(划重点)
- 解题思路是: 先对最小的区间进行排序,然后将两个排序子区间合并;然后再向上递归处理。
- 需要注意的是,每个排好序的小区间需要断开,即末尾要指向nullptr,方便下一步的链表合并。
- 在将一个小区间划分成两个部分时,可以用快慢指针快速实现。
- 在链表合并时,最后剩余的而一条链的判断语句是
if
,不是while
,这里写错几次了。
No.234 判断一个链表是否是回文链表 (AC)
- BUG思路:将整个链表翻转,然后比较翻转前后的链表是否相同。这种做法存在的问题是, 完成翻转之后,原链表的头指针已经不在是原链表的头指针,即只能维持一条链表的完整,不能同时保证翻转前后的链表完整。
- 改进思路: 将链表后半部分翻转,翻转后比较前后时候是完全相同的。在比较的时候只要处理n/2长度的,这样不管是奇偶都可以解决。
No.237 删除链表中的某个结点 (AC)
- 当前删除结点的下一个结点为尾结点,则直接将当前结点赋值为nullptr;
- 将要删除的结点的下一个结点的值赋给当前结点,然后将当前结点的下一个结点删除。
No.328 将原链表中奇数位置的结点放在一起,后面接偶数位置的结点 (AC)
- 新建两个表头,分别是奇数位置结点和偶数位置结点的表头,然后将原链表中相应结点临界点两个头结点后边,再把偶数头结点链到奇数链表的结尾。特别需要注意的是,偶数链表的为节点需要指向nullptr。否则无法打印出结果,显示TLE。