链表经典面试题

1. 删除链表定值 val 所有结点

OJ链接

    public ListNode removeElements(ListNode head, int val) {
        if(head == null){
            return null;
        }else{
            ListNode cur = head.next;
            ListNode prev = head;
            while(cur!=null){
                if(cur.val == val){
                    prev.next = cur.next;
                }else{
                    prev = cur;
                }
                cur = cur.next;
            }
            if(head.val == val){
                head = head.next;
            }
            return head;
        }
    }

错误代码示例
删除链表中第一次出现的关键字 val 的代码是上来就判断头结点 head 是否需要删除。而删除所有关键字 val 时候需要把头结点放在最后判断删除

    public ListNode removeElements(ListNode head, int val) {
        if(head == null){
            return null;
        }else{
        	// 删头结点
        	if(head.val == val){
				head = head.next;
			}
			// 删剩余结点
            ListNode cur = head.next;
            ListNode prev = head;
            while(cur!=null){
                if(cur.val == val){
                    prev.next = cur.next;
                }else{
                    prev = cur;
                }
                cur = cur.next;
            }
            if(head.val == val){
                head = head.next;
            }
            return head;
        }
    }

思路

  1. 先判断连表是否为空
  2. 找到被删结点 cur,让前驱节点 prev 指向被删结点 cur 的下一个节点 cur.next;如果不是要删除的结点则让 prev 指向 cur 本身
  3. 最后判度是否删除头结点head会把链表第二个节点 head.next 遗漏掉

注意

  1. 头结点边界条件不可省略
  2. 要删除一个结点必须要知道它的前驱 prev 和 后继 cur.next,所以需要两个结点
  3. while 循环中是 cur!=null 。因为要判断每一个节点而不是前一个结点,所以不是 cur.next!=null
  4. 如果值相等,prev 指向 cur.next;如果不相等,则不需要删除直接让 prev=cur 即可
  5. 最后判断头结点 head 是否需要删除的原因:如果第一步删除头结点 head,会遗漏掉第二个节点
	// 删头结点
	if(head.val == val){
		head = head.next;
	}
	// 删剩余结点
	ListNode cur = head.next;
	ListNode prev = head;

解释第二个结点遗漏的图【删除所有值为 2 的结点】
在这里插入图片描述
假设头结点也是我们需要删除的结点,我们发现第二个结点0x987这个结点会被跳过,直接从第三个节点0x167开始判断 cur.val== val,因此需要最后判断头结点是否需要删除

2. 反转单链表

Oj链接

    public ListNode reverseList(ListNode head) {
        ListNode cur = head;
        ListNode prev = null;
        ListNode curNext = null;
        while(cur != null){
            curNext = cur.next;
            cur.next = prev;
            prev = cur;
            cur = curNext;
        }
        return prev;
    }

思路

三指针移动。prev:前驱、cur:当前、curNext:后继

  1. 头结点的边界条件可以不用判断,因为为空的时候 返回的也是 prev 也为空
  2. 保留当前节点 cur 的 next 域
  3. 当前结点 cur 指向 前一结点 prev,此时 prev 相当于反转后的 尾结点
  4. 移动前驱结点 prev 到当前结点 cur 位置
  5. 把保留的第一步中 cur.next 域让 cur 走向下一个结点 curNext 带动 cur 移动

注意

  1. cur 从 head 开始移动
  2. 因为遍历所有节点所以循环条件是:cur!=null
  3. cur 的移动并不能用 cur=cur.next 代替,因为在第一次 cur.next = prev的时候由于 prev是 null,所以会导致 cur的next域为 null,当 cur=cur.next 的时候会引发空指针异常,所以需要额外的结点curNext来保存 cur的后继结点 cur.next
  4. 最后返回的是 prev结点,才是反转后的头结点

3. 链表第二个中间结点

Oj链接

    public ListNode middleNode(ListNode head) {
        if(head == null){
            return null;
        }else{
            ListNode fast = head;
            ListNode slow = head;
            while(fast != null && fast.next != null){
                fast = fast.next.next;
                slow = slow.next;
            }
            return slow;
        }
    }

思路

  1. 边界条件空结点判断不可省略
  2. 快慢双指针移动【快结点每次移动2步,慢结点移动1步】
  3. 循环判断到空结点结束当前循环
  4. 返回慢结点,慢结点就是当前的中间结点

注意

  1. fast!=null:当元数个数为偶数的时候,会防止快指针空指针异常
  2. fast.next!=null:当元数个数为奇数的时候,会防止快指针空指针异常
  3. 综合考虑链表元数个数,所以需要用 && 把边界条件结合起来才可以放置因为链表元数个数导致的空指针异常

4. 链表中倒数第k个结点

Oj链接

    public ListNode FindKthToTail(ListNode head,int k) {
        ListNode fast, slow;
        for(fast = slow = head; fast != null; fast=fast.next,--k){
            if(k<=0){
                slow = slow.next;
            }
        }
        return k<=0?slow:null;
    }

思路

  1. 头结点边界条件不可省略
  2. 快慢指针都从 head 起步,且让快指针 fast 先行,当走完 k 步后才让慢指针 slow 再走。且快慢指针每次均移动1步
  3. 只有当 k<=0 成立之后才返回满指针 slow

注意

1.快指针 fast 走完 k 步之后在移动满指针 slow,当 fast 移动到末尾的时候,slow 就是倒数第 k 个结点
2. 返回值需要注意:当链表走完之后【fast 走到尾结点之后的节点】k 还未走完,则此时的 slow 就不属于倒数第 k 个结点。所以需要返回的时候判断 k 是否走完了再决定返回 slow

5. 合并两个有序链表

Oj链接

	public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
	        ListNode newHead = new ListNode(-1);
	        ListNode tmp = newHead;
	        while(l1 != null && l2 != null){
	            if(l1.val <= l2.val){
	                tmp.next = l1;
	                l1 = l1.next;
	            }else{
	                tmp.next = l2;
	                l2 = l2.next;
	            }
	            tmp =tmp.next;
	        }
	        if(l1 == null){
	            tmp.next = l2;
	        }
	        if (l2 == null){
	            tmp.next = l1;
	        }
	        return newHead.next;
	    }

思路

  1. 创建新的头结点 newHead 来保存头结点、临时结点 tmp 进行一步一步移动记录
  2. 遍历 l1 和 l2 两个链表的结点
  3. 对比两个结点的值域 val 的大小,把小的拼接在 tmp 的后边 next 域。
  4. 在拼接过程中 l1、l2、tmp都需要向后移动一步
  5. 退出循环时候在 if 判断谁导致的循环终止
  6. 若 l1 走到结尾,则 tmp 直接拼接剩余的 l2;若 l2 走到结尾,则 tmp拼接剩余的 l1【因为都是升序,所以不用担心直接拼接后的大小排序问题】

注意

  1. while 循环之后必须要 if 判断 while 循环结束条件,然后再进行尾部拼接
  2. 还要注意可能一个都不满足和 x 值大小关系的情况。进行另外的拼接
    在这里插入图片描述

6. 链表分割

OJ链接

    public ListNode partition(ListNode pHead, int x) {
        ListNode cur = pHead;
        ListNode bs = null;
        ListNode be = null;
        ListNode as = null;
        ListNode ae = null;
        while(cur != null){ 
            if(cur.val < x){
                if(bs == null){
                    bs = cur;
                    be = cur;
                }else{
                    be.next = cur;
                    be = be.next;
                }
            }else{
                if(as == null){
                    as = cur;
                    ae = cur;
                }else{
                    ae.next = cur;
                    ae = ae.next;
                }
            }
            cur =cur.next;
        }
        if(bs == null){
            return as;
        }else{
            be.next = as;
            if(as != null){
                ae.next = null;
            }
            return bs;
        }
    }

思路

  1. 小于 x 的结点由 bs,be组成;大于或等于 x 的结点由as,ae组成
  2. while 循环遍历所有结点
  3. while 循环结束后再判断bs,as的为空这一特殊情况

注意

  1. 无论 bs 还是 as 在拼接的时候需要留意第一次拼接情况,因此需要额外判断结点为空的这一特殊情况
  2. 每次结点的 next 域指向完毕之后该节点必须要向下移动一步,让该节点移动到被拼接结点

7. 删除链表重复节点

Oj链接

    public ListNode deleteDuplication(ListNode pHead) {
        ListNode newHead = new ListNode(-1);
        ListNode tmp = newHead;
        ListNode cur = pHead;
        while(cur != null){
            if(cur.next != null && cur.val == cur.next.val){
                while(cur.next != null && cur.val == cur.next.val){
                    cur = cur.next;
                }
            }else{
                tmp.next = cur;
                tmp = tmp.next;
            }
            cur = cur.next;
        }
        tmp.next = null;
        return newHead.next;
    }

思路

  1. 创建一个傀儡头结点 newHead 用来保存头结点
  2. 创建一个 tmp 结点用来移动串连后续结点
  3. 创建一个 cur 结点来移动单链表
  4. while 循环遍历每一个结点
  5. if 判断找到相等结点后就利用 while 循环删掉剩余的相等节点
  6. cur 遍历完之后 tmp 就是尾结点,需要置为 null
  7. 返回傀儡结点的下一个结点

注意

  1. 外层 while 循环因为要遍历每一个结点,所以是 cur!=null
  2. 内层的 if 判断 cur.next!=null 是为了判断是不是只有一个尾结点
  3. 内层的 while 循环判断是为了判断该结点之后还有没有相等结点。有:则 cur=cur.next 进行删除
  4. if 判断无论成不成立都需要 cur=cur.next 进行移动下一步【如果不移动,遇到相等结点则会保留相等的最后一个位置结点而不删除】
  5. 循环完毕之后假设多个尾结点相等的话,则会保留相等尾结点的前一个结点,此时该结点的 next 不为 null,因此需要手动设置尾结点为 null

8. 链表回文结构

Oj链接

public boolean chkPalindrome(ListNode A) {
        // write code here
        if (A == null) {
            return true;
        }
        if (A.next == null) {
            return true;
        }
        ListNode fast = A;
        ListNode slow = A;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode cur = slow.next;
        ListNode curNext = null;
        while (cur != null) {
            curNext = cur.next;
            cur.next = slow;
            slow = cur;
            cur = curNext;
        }
        while (A != slow) {
            if (A.val != slow.val) {
                return false;
            }
            if (A.next == slow) {
                return true;
            }
            A = A.next;
            slow = slow.next;
        }
        return true;
    }

思路

  1. 快慢指针找到中间结点
  2. 中间结点到尾结点部分全部反转
  3. 头尾遍历一 一对比

注意

  1. 快指针每次移动 2 步,慢指针每次移动 1 步
  2. 反转需要从中间结点的下一个结点开始反转
  3. 反转完毕之后需要一个从头走一个从尾走进行比对结点是否相等,若有一个不相等就判定 false
  4. 需要额外注意链表元数个数影响,当为偶数的时候需要再判断 头结点和尾结点 判断相等后再接着判断中间结点的下一个结点是否和 slow 相等,如图所示【head向右走,slow向左走】

在这里插入图片描述

9. 两个链表第一个公共结点

Oj链接

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA = 0;
        int lenB = 0;
        ListNode pA = headA;
        ListNode pB = headB;
        while(pA != null){
            ++lenA;
            pA = pA.next;
        }
        while(pB != null){
            ++lenB;
            pB = pB.next;
        }
        int sub = lenA-lenB;
        if(sub > 0){
            pA = headA;
            pB = headB;
        }else{
            pA = headB;
            pB = headA;
            sub = lenB - lenA;
        }
        while(sub != 0){
            --sub;
            pA = pA.next;
        }
        while(pA != pB && pA != null && pB != null){
            pA = pA.next;
            pB = pB.next;
        }
        if(pA == pB && pA != null){
            return pA;
        }else{
            return null;
        }
    }

思路

  1. 计算出两个单链表的长度
  2. 让长的单链表走差值步
  3. 在“同一起跑线”后开始移遍历两个单链表,判断是否相遇即可

注意

  1. 计算完长度之后需要将 pA、pB进行重置初始头结点位置。不用链表的头结点移动原因是需要记录初始结点位置
  2. while 循环条件的判断。一直遍历到 null 和两个结点不相等。这样当循环退出的时候 if 在做取舍判断
  3. if 需要判断什么原因导致的 while 循环终止后再给定返回值

在这里插入图片描述

10. 链表是否有环

Oj链接

    public boolean hasCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                return true;
            }
        }
        return false;
    }

思路

  1. 快慢指针:移动 2 步、慢指针移动 1 步
  2. 如果两个指针相遇了就代表有环

注意

  1. while 循环的条件要考虑元素个数的奇偶
  2. fast.next是为了防止当个数为偶数的时候发生越界空指针异常
  3. fast是为了放置当个数为奇数打的时候发生越界空指针异常

分析
①一个走2步,一个走1步

快结点慢结点
333222
555333
333444
555555
②一个走3步,一个走1步
快结点慢结点
444222
444333
444444
对比中我们发现,3步走的速度虽然快,但是中间会省略掉更多的结点,但是方案①而言,快慢结点步数差1,快结点每次走2步,慢结点每次走1步。对于链表元素较大的时候可以遍历的次数更少一点[途中列举的5个元素,虽然方案②比方案①更快的找到环结点。但一般而言,方案①会更好]
最佳方法:一个走一步,一个走两步[一个走三步,一个走四步: 这种情况要么相遇时间变慢,要么无法相遇]
在这里插入图片描述

11. 链表入环的第一个节点

Oj链接

    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){
                break;
            }
        }
        if(fast == null || fast.next == null){
            return null;
        }else{
            slow = head;
            while(fast != slow){
                fast = fast.next;
                slow = slow.next;
            }
            return slow;
        }
    }

思路

  1. 快指针:每次移动 2 步、慢指针移动 1步
  2. 当两个指针相遇到时候退出循环
  3. 判断什么原因导致的推出循环
  4. 慢结点重置到头结点位置
  5. while 继续遍历快慢结点,直到相遇就退出循环。此时慢结点就是我们需要的入口结点

注意

  1. 快退出的条件判断:如果因为走到尾结点而退出循环,则连环都没有。因此返回null
  2. fast保持在相遇节点,让slow移动。依靠slow.next即可找到与fast相遇的节点

在这里插入图片描述
分析

  1. 慢结点速度是快结点的一半
  2. 因此总路程而言快结点是慢结点的二倍
  3. 慢结点总路程:X+Y
  4. 快结点总路程:X+Y+NC
  5. 2*(X+Y) = X+Y+NC【化简后:x = NC-Y】
  6. 说明走的环的路程是一个定值,假设N为1,则C的长度为10,N为2,C为5.

在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值