一、前言
书接上文,在梳理完链表的基础过后,即链表的创建以及基本的增删查改后,经过了4天左右的时间,把链表涉及到的一些高频题刷完了,趁热打铁,通过本文记录刚开始刷题遇到的问题,并总结链表题几种基本的解题思路。当然,题目很多,本文主要记录个人在刷题过程中开始没有思路或者虽然有思路但是花去不少时间一直出错的题!
二、部分高频题
2.1 链表合并
我最开始的思路是,将两个链表先全部放入队列里面去,然后出队列进行比较,再依次拼接。首先这个思路肯定是可以实现的,并且我很快通过LinkedList实现了,但是实现后发现时间花费很不理想,才突然发现为什么不直接在原链表上取值比较呢?这样明明时间空间都得到了节省,实现如下:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1==null){
return list2;
}
if(list2==null){
return list1;
}
ListNode headC = null;
ListNode cur = null;
//确定头结点
if(list1.val<=list2.val){
headC = list1;
list1=list1.next;
}else{
headC = list2;
list2=list2.next;
}
cur = headC;
//直接在原链表上取值
while(list1!=null&&list2!=null){
if(list1.val<=list2.val){
cur.next = list1;
list1 = list1.next;
}else{
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
//剩余的链表连接上表尾
cur.next = list1==null ? list2:list1;
return headC;
}
}
注意:这里还有一点是我优化后才改的,就是剩余链表连接上表尾那一块,我刚开始没用充分理解链表的优势,完全以数组的思想再通过while循环依次赋值,其实正如我们的list集合一样,链表的直接拼接正是其一大优势!!!
2.1.1 思路拓展
基于两个链表的实现,那么是多个链表的合并呢?
思路事是一样的,两个链表的拼接作为方法封装,循环拼接即可,这是我们在面试时遇到时可以想到的最快方式。
2.2 寻找和反转
这一类的题目都离开不开一个特性,那就是为我们在实现时,往往需要对两个节点进行同步操作,最为经典的就是寻找倒数第K个节点。
这里一个非常巧妙的点就是,通过快慢指针帮助我们找到倒数第K个节点在正数的第几个,如下图所示:
以数学来说,就是 k = length - (length-k),那么我们思路清晰了,实现也就不难了。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
//还可以优化,减少遍历次数
ListNode slow = head;
ListNode fast = head;
while(fast!=null&&k>0){
fast=fast.next;
k--;
}
while(fast!=null){
slow=slow.next;
fast=fast.next;
}
return slow;
}
}
双指针的妙用,还远远不止于此,下一个经典题莫过于链表旋转。
思路:这和我们寻找倒数第K个节点类似,通过快慢指针我们可以得到链表需要右移的开始节点,也就是需要断开的节点。当然,这里面还有其他的一些细节,首先就需要注意右移k个位置,这个k个很有可能比链表长度长,所以我们需要先计算链表长度,取k%length为真正的右移位数。
代码实现:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head==null){
return null;
}
int length = getLength(head);
int l = k%length;
if(length==1||l==0){
return head;
}
ListNode slow = head;
ListNode fast = head;
ListNode newNode = null;
while(l>0){
l--;
fast = fast.next;
}
//fast先走k步,在一起走,fast到尾结点,slow刚好到达倒数第k个点
while(fast.next!=null){
fast = fast.next;
slow = slow.next;
}
newNode = slow.next;
fast.next =head;
slow.next = null;
return newNode;
}
//获取链表长度
public int getLength(ListNode head){
int length = 0;
while(head!=null){
length++;
head =head.next;
}
return length;
}
}
当然,双指针用途远不止此,更多灵活的题目需要我们在此思想的基础上理清逻辑!!
2.3 链表删除
这里最为重要的点在于:在删除单个节点或多个相同节点(不需要保留下一个),虚拟节点带来的重要之处!!!让需要被删除的节点的上个节点可以指向需要被删除的下一个节点,而在判断时保证了不会出现空指针的情况
这能够使得我们在不清楚删除的是头结点或是其他节点的情况下,依旧可以在一个情况的思路下涵盖删除首、中、尾三种我们在基础删除时提出的情况,我们以以下题为例:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode dumpNode = new ListNode(0);
dumpNode.next = head;
ListNode cur = dumpNode;
while(cur.next!=null&&cur.next.next!=null){
if(cur.next.val == cur.next.next.val){
int x = cur.next.val;
while(cur.next!=null&&cur.next.val==x){
cur.next = cur.next.next;
}
}else{
cur = cur.next;
}
}
return dumpNode.next;
}
}
那么对于重复的链表去重时,依旧使用虚拟节点会带来比较多的麻烦,对于后两个节点可能不得不拿出来单独处理,这时我们的思想需要一定的改变,当前节点的值替换成下一个节点的值,再让当前节点指向下下个节点。这就有点类似一头应该被宰的狼却把羊宰了,然后把他的皮披在自己身上,从而避开了杀身之祸,哈哈哈
我们以一个例题来说明:
代码实现:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode cur =head;
while(cur!=null&&cur.next!=null){
if(cur.val==cur.next.val){
cur.next = cur.next.next;
}else{
cur=cur.next;
}
}
return head;
}
}
三、总结
到此,链表的几个自己认为比较有区别的高频题分析到此为止。对于链表相关的题,基本可以从hash、双指针、栈、链表本身特性这几个方面去解决,对于此类题目一般不会太难,更多需要仔细判别,逻辑上要连贯,不让其出现空指针的情况,更多是代码细节中的问题,这点我认为作为算法入门是恰到好处的!