链表高频面试题(算法村第一关白银挑战)

1 链表相交

题目描述

面试题 02.07. 链表相交 - 力扣(LeetCode)

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

图示两个链表在节点 c1 开始相交**:**

链表高频面试题(算法村第一关白银挑战)_第1张图片

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

示例 1:

链表高频面试题(算法村第一关白银挑战)_第2张图片

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A[4,1,8,4,5],链表 B[5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

解法一:哈希表

public ListNode getIntersectionNode(ListNode headA, ListNode headB)
{
    HashSet<ListNode> hashSet = new HashSet<>();

    while (headA != null)
    {
        hashSet.add(headA);
        headA = headA.next;
    }

    while (headB != null)
    {
        if(hashSet.contains(headB))
            return headB;

        headB = headB.next;
    }

    return null;
}

解法二:栈

public static ListNode getIntersectionNode(ListNode headA, ListNode headB)
{
    ArrayDeque<ListNode> stackA = new ArrayDeque<>();
    ArrayDeque<ListNode> stackB = new ArrayDeque<>();

    //两个链表分别入不同的栈
    while (headA != null)
    {
        stackA.push(headA);
        headA = headA.next;
    }
    while (headB != null)
    {
        stackB.push(headB);
        headB = headB.next;
    }


    ListNode intersectionNode = null;   //要跟着两链表相同的结点

    //从后往前理解(栈先进后出的特性)
    while (!stackA.isEmpty() && !stackB.isEmpty())
    {
        if (stackA.peek().val == stackB.peek().val)
        {
            intersectionNode = stackA.pop();
            stackB.pop();
        }
        else
            break;  //两链表开始分叉,intersectionNode也指向了第一个交汇结点
    }

    return intersectionNode;

解法三:换表双指针

public ListNode getIntersectionNode(ListNode headA, ListNode headB)
{
    //排除掉不好统一处理的空链表情况
    if(headA == null || headB == null)
        return null;

    ListNode p = headA;
    ListNode q = headB;

    while(p != q)
    {
        p = p.next;
        q = q.next;

        if(p != q)
        {
            //自己的链表遍历完了就跳到另一个链表一直遍历下去,直到相遇,或者都等于null
           if(p == null)
               p = headB;
           if(q == null)
               q = headA;
        }

    }
    //return得放在外面,否则编译报错:无返回语句
    return p;
}

解法四:差和双指针

或称快慢指针。长链表的指针比短链表的指针先走|len1 - len2|步,然后同步移动,指向同一结点时返回答案

public ListNode getIntersectionNode(ListNode headA, ListNode headB)
{
    if(headA == null || headB == null)
        return null;

    //获取headA的表长
   ListNode p = headA;
   int len_A = 0;
   while (p != null)
   {
       len_A++;
       p = p.next;
   }

    //获取headB的表长
    ListNode q = headB;
    int len_B = 0;
    while (q != null)
    {
        len_B++;
        q = q.next;
    }

    int diff = Math.abs(len_A - len_B);

    if(len_A < len_B)   //headB先走
    {
        while (diff != 0)
        {
            headB = headB.next;
            diff--;
        }
    }
    else if(len_A > len_B)   //headA先走
    {
        while (diff != 0)
        {
            headA = headA.next;
            diff--;
        }
    }

    while (headA != headB)
    {
        headA = headA.next;
        headB = headB.next;
    }

    return headA;
}

2 回文链表

题目描述

234. 回文链表 - 力扣(LeetCode)

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

示例 1:

img

输入:head = [1,2,2,1]
输出:true

解法一:值复制

虽然这么做简单易懂,但面试手撕算法题时不推荐,因为会被视为逃避链表,失去了考验价值

    public boolean isPalindrome(ListNode head) 
    {
        //将每一个结点的值复制到ArrayList。
        ArrayList<Integer> vals = new ArrayList<>();

        while(head != null)
        {
            vals.add(head.val);
            head = head.next;
        }

        //进行回文判断
        int left = 0;
        int right = vals.size() - 1;

        while(left < right)
        {
            if(vals.get(left).equals(vals.get(right)) == false)
                return false;
            left++;
            right--;
        }

        return true;
    }

解法二:快慢指针+反转链表

//寻找中间结点。若链表有奇数个节点,则中间的节点应该看作是前半部分。
    private ListNode endOfFirstHalf(ListNode head)
    {
        ListNode fast = head;
        ListNode slow = head;
        while(fast.next != null && fast.next.next != null)
        {
            fast = fast.next.next;
            slow = slow.next;
        }

        return slow;
    }

    //反转链表
    private ListNode reverseList(ListNode head)
    {
        ListNode pre = null;
        ListNode suc = head;

        while(head != null)
        {
            suc = head.next;
            head.next = pre;     
            pre = head;     
            head = suc;     
        }

        return pre;
    }

    public boolean isPalindrome(ListNode head) 
    {
        if(head == null)
            return true;
        
        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode firstHalfEnd = endOfFirstHalf(head);
        ListNode secondHalfStart = reverseList(firstHalfEnd.next);

        //判断是否是回文链表
        ListNode p = head;
        ListNode q = secondHalfStart;
        boolean ans = true;
        while(q != null)
        {
            if(p.val != q.val)
            {
                ans = false;
                break;
            }

            p = p.next;
            q = q.next;      
        }

        //恢复后半部分链表
        firstHalfEnd.next = reverseList(secondHalfStart);

        //返回判断结果
        return ans;
    }

解法三:栈

public static boolean isPalindrome(ListNode head)
    {
        ArrayDeque<ListNode> stack = new ArrayDeque<>();

        ListNode t = head;
        while (t != null)
        {
            stack.push(t);
            t = t.next;
        }

        while (head != null)
        {
            if(stack.pop().val != head.val)
                return false;
            head = head.next;
        }

        return true;
    }

3 合并链表

3.1 合并两个有序链表

题目描述

21. 合并两个有序链表 - 力扣(LeetCode)

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:

链表高频面试题(算法村第一关白银挑战)_第3张图片

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
public ListNode mergeTwoLists(ListNode list1, ListNode list2) 
    {
        if(list1 == null)
            return list2;
        if(list2 == null)
            return list1;

        ListNode p = list1; //新的引用
        ListNode q = list2;
        ListNode head = new ListNode(0 , null); //新链表的头结点
        ListNode r = head;

        while(p != null  && q != null)
        {
            //一次只处理一个结点
            if(p.val <= q.val)  
            {
                r.next = p;
                p = p.next;
            }
            else
            {
                r.next = q;
                q = q.next;
            }

            r = r.next; //先链接,后更新
        }

        if(p == null)
            r.next = q;
        if(q == null)
            r.next = p;

        return head.next;
    }

3.2 合并K个有序链表

在3.1的基础上

public ListNode mergeKLists(ListNode[] lists) 
{
	ListNode res = null;
    //逐一链接
	for (ListNode list: lists)
		res = mergeTwoLists(res,lists);
	
    return res;
}

3.3 嫁接两个链表

题目描述

1669. 合并两个链表 - 力扣(LeetCode)

给你两个链表 list1list2 ,它们包含的元素分别为 n 个和 m 个。

请你将 list1 中下标从 ab 的全部节点都删除,并将list2 接在被删除节点的位置。

下图中蓝色边和节点展示了操作后的结果:

链表高频面试题(算法村第一关白银挑战)_第4张图片

请你返回结果链表的头指针。

示例 1:

链表高频面试题(算法村第一关白银挑战)_第5张图片

输入:list1 = [0,1,2,3,4,5], a = 3, b = 4, list2 = [1000000,1000001,1000002]
输出:[0,1,2,1000000,1000001,1000002,5]
解释:我们删除 list1 中下标为 34 的两个节点,并将 list2 接在该位置。上图中蓝色的边和节点为答案链表。
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
    ListNode pre = list1;
    ListNode post = list1;
    int i = 0;
    int j = 0;

    //定位待删除部分的前驱和后继
    while (i < a - 1) { //用<号,i=a-2时最后一次进入循环,退出循环时pre刚好指向下标为a-1的结点
        pre = pre.next;
        i++;
    }

    while (j < b + 1) {
        post = post.next;
        j++;
    }

    //定位list2的尾结点
    ListNode r = list2;
    while (r.next != null)
        r = r.next;

    //连接
    pre.next = list2;
    r.next = post;

    return list1;
}

4 双指针专题

4.1 寻找中间结点

题目描述

876. 链表的中间结点 - 力扣(LeetCode)

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:

img

输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3
    public ListNode middleNode(ListNode head)
    {
        ListNode p = head;
        ListNode q = head;

    	//p每次走两步,q每次走一步
    	//p走到最后的NULL时,q恰好到中间
    	//偶数个结点的话,q则指向第二个中间结点
    	while(p !=null && p.next != null)
    	{
        	p = p.next.next;
        	q = q.next;
    	} 

    	return q;
    }

4.2 寻找倒数第K个元素

解法一:两次遍历

第一次遍历得到链表长度,第二次遍历找倒数K个元素

解法二:快慢指针

两种模板

public ListNode getNthFromEnd(ListNode head, int n)
{
    ListNode fast = head;
    ListNode slow = head;

    while (fast != null)
        {
            if(n <= 0)   
            	slow = slow.next; 
            
        	fast = fast.next;
        	n--;
        }
    
    return slow;
}
public ListNode getKthFromEnd(ListNode head, int k)
    {
    ListNode fast = head;
    ListNode slow = head;

    while(fast != null && k > 0)
    {
        fast = fast.next;
        k--;
    }
    
    while(fast != null)
    {
        fast = fast.next;
        slow = slow.next; 
    }
    
    return slow;
}

4.3 旋转链表

题目描述

61. 旋转链表 - 力扣(LeetCode)

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

示例 1:

链表高频面试题(算法村第一关白银挑战)_第6张图片

输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]

示例 2:

链表高频面试题(算法村第一关白银挑战)_第7张图片

输入:head = [0,1,2], k = 4
输出:[2,0,1]
public ListNode rotateRight(ListNode head, int k)
{
    if (head == null || k == 0)
        return head;

    //获取链表长度
    ListNode t = head;
    int length = 0;
    while (t != null)
    {
        length++;
        t = t.next;
    }

    int K = k % length; //最小旋转次数
    if(K == 0)
        return head;
    //寻找倒数第K个结点的前驱
    ListNode fast = head;
    ListNode slow = head;   //倒数第K个结点的前驱
    while (fast.next != null)   //fast需要停留在最后一个结点
    {
        if(K <= 0)
            slow = slow.next;

        fast = fast.next;
        K--;
    }

    ListNode res = slow.next;   //旋转过后的链表的头结点为倒数第K个结点
    slow.next = null;  //断开前驱与倒数第K个结点的链接,避免成环。即slow作为新链表的尾结点
    fast.next = head;   //原尾结点连接原头结点,"旋转"完成

    return res;
}

5 删除链表元素

5.1 删除特点结点

题目描述

203. 移除链表元素 - 力扣(LeetCode)

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1:

链表高频面试题(算法村第一关白银挑战)_第8张图片

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
解法一:单独处理头结点
/**
 * 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 removeElements(ListNode head, int val) 
    {
        //val出现在头结点
        while(head != null && head.val == val)  
            head = head.next;
    
         if(head == null)
            return head;  //直接解决全部都是val的情况


    ListNode p = head;

    while(p != null && p.next != null)  //常规情况
    {
        if(p.next.val == val)
        {
            p.next = p.next.next;  
        }
        else
        {
            p = p.next;
        }
    }

    return head;
    }
}
解法二:虚拟结点
public ListNode removeElements(ListNode head, int val)
{
    ListNode dummyNode = new ListNode(-1);
    dummyNode.next = head;  //为将头结点当作普通结点处理
    ListNode cur = dummyNode;

    while (cur.next != null)    //如果尾结点也是要删除的结点,那它也能被“跳过”。要时刻谨记.next存的是下一结点的地址,不是一根线
    {
        if(cur.next.val == val)
            cur.next = cur.next.next;   //cur所指结点的后继变了,且cur仍然指向当前结点
        else
            cur = cur.next; //下一个结点的值(终于)不重复时,cur才指向下一个结点
    }

    return dummyNode.next;
}

5.2 删除倒数第n个结点

题目描述

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

链表高频面试题(算法村第一关白银挑战)_第9张图片

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
解法一:两次遍历
struct ListNode* removeNthFromEnd(struct ListNode* head, int n)
{
    //计算链表长度
    struct ListNode* fast = head;
    struct ListNode* slow = head;

    while(fast)
    {
        if(n < 0)
            slow = slow->next; //慢指针开始与快指针同步移动
        fast = fast->next;
        n--;
    }

    if(n == 0)  //要删除的是头结点,返回其余结点
        return head->next;

    slow->next = slow ->next->next; //删除指定结点

    return head;      
}

解法二:快慢指针一次遍历
public ListNode removeNthFromEnd(ListNode head, int n)
    {
         //计算链表长度
    	ListNode fast = head;
    	ListNode slow = head;

    	while(fast != null)
    	{
        	if(n < 0)	//慢指针开始与快指针同步移动,最终slow会指向待删除结点的前驱
            	slow = slow.next; 
        	fast = fast.next;
        	n--;
    	}

    	if(n == 0)  //若删除的是头结点,则返回其余结点
        	return head.next;

    	slow.next = slow.next.next; //删除指定结点

    	return head; 
    }

5.3 重复元素保留一个

题目描述

83. 删除排序链表中的重复元素 - 力扣(LeetCode)

给定一个已排序的链表的头 head删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表

示例 1:

链表高频面试题(算法村第一关白银挑战)_第10张图片

输入:head = [1,1,2]
输出:[1,2]

示例 2:

链表高频面试题(算法村第一关白银挑战)_第11张图片

输入:head = [1,1,2,3,3]
输出:[1,2,3]
public ListNode deleteDuplicates(ListNode head)
{
    if (head == null)
        return null;

    ListNode cur = head;

    while (cur.next != null)
    {
        if(cur.val == cur.next.val)
            cur.next = cur.next.next;
        else
            cur = cur.next;
    }

    return head;
}

5.4 重复元素一个都不要

题目描述

82. 删除排序链表中的重复元素 II - 力扣(LeetCode)

给定一个已排序的链表的头 head删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表

示例 1:

链表高频面试题(算法村第一关白银挑战)_第12张图片

输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]

示例 2:

链表高频面试题(算法村第一关白银挑战)_第13张图片

输入:head = [1,1,1,2,3]
输出:[2,3]
public ListNode deleteDuplicates(ListNode head)
{
    ListNode dummyNode = new ListNode(0, head);
    ListNode cur = dummyNode;

    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    //如if被执行,则cur不会往后移动,这使得两个while可以合力一口气删完如[2,2,3,3,5,5,5]的重复部分,然后cur才需要往后移动
            cur = cur.next;
    }

    return dummyNode.next;
}

你可能感兴趣的:(算法村,链表,数据结构,力扣,算法)