编程导航算法通关第1关|白银教程 学习

链表面试算法题

本章节主要总结了关于单链表的高频面试题。


本章的链表定义

public class ListNode {
    public int val;
    public ListNode next;

    public ListNode(int x) {
        val = x;
        next = null;
    }

    public static void main(String[] args) {
        ListNode listnode=new ListNode(1);
    }
}

两个链表的第一个公共子节点问题

链表相交
两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。
题目的解法:

如果没有思路的话,把 将常用数据结构和常用算法思想都想一遍
数组、链表、队列、栈、Hash、集合、树、堆

  1. 暴力法:类似于冒泡排序,将第一个链表中的各个结点与第二个链表中的各个结点进行比较,当出现相等的结点指针时,即为相交结点。
  2. Hash。第一个集合存放在map,set,或者Arraylist集合中,利用constains方法检测第二个链表中哪个结点出现过,如果有相同的节点指针,即找到。
  3. 栈。将两个链表分别压入不同的栈中,同时弹出栈顶元素,判断该元素是否一致。一致则存在相交。当首次出现不相同时,则最晚出栈的那一组即要找的位置。
  4. 拼接法(难想到)。链表A:2-3-4-5 链表B:b-4-5;将其组合为AB,BA。以公共的起始点分成两部分,就可以得到LA RA LB RB 和 LB RB LA RA。实际上是RB 和RA相同的时候,则得到了两个链表的合并点。
  5. 差和双指针。当链表A,链表B的长度相同时,两个指针同时开始遍历元素时,才有机会对等的遇到两个链表的合并点。设链表A 的长度为lenA,设链表B 的长度为lenB。那么,|lenA - lenB|的差值,是长度更长的先走|lenA -lenB|步,然后再同时向前走,结点一样的时候就是公共结点了。
// 利用set
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
        Set<ListNode> set = new HashSet<>();
        while (headA != null) {
            set.add(headA);
            headA = headA.next;
        }

        while (headB != null) {
            if (set.contains(headB))
                return headB;
            headB = headB.next;
        }
        return null;
}

// 利用栈
public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
        Stack<ListNode> stackA = new Stack();
        Stack<ListNode> stackB = new Stack();
        while (headA != null) {
            stackA.push(headA);
            headA = headA.next;
        }
        while (headB != null) {
            stackB.push(headB);
            headB = headB.next;
        }

        ListNode preNode = null;
        while (stackB.size() > 0 && stackA.size() > 0) {
            if (stackA.peek() == stackB.peek()) {
                preNode = stackA.pop();
                stackB.pop();
            } else {
                break;
            }
        }
        return preNode;
    }
// 利用序列拼接
public static ListNode findFirstCommonNodeByCombine(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null) {
            return null;
        }
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        while (p1 != p2) {
            p1 = p1.next;
            p2 = p2.next;
            if (p1 != p2) {
                if (p1 == null) {
                    p1 = pHead2;
                }
                if (p2 == null) {
                    p2 = pHead1;
                }
            }
        }
        return p1;
    }
// 利用差 和 指针
public static ListNode findFirstCommonNodeBySub(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null) {
            return null;
        }
        int lenA=0,lenB=0;

        // 获取长度
        ListNode curNode=pHead1;
        while(curNode!=null){
            curNode=curNode.next;
            lenA++;
        }

        curNode=pHead2;
        while(curNode!=null){
            curNode=curNode.next;
            lenB++;
        }

        // 获取差值
        curNode=pHead1;
        ListNode curNode2=pHead2;
        int sub=Math.abs(lenA-lenB);
        if(lenA>=lenB){
            int count=0;
            while (count<sub){
                curNode=curNode.next;
                count++;
            }
        }else {
            int count=0;
            while (count<sub){
                curNode2=curNode.next;
                count++;
            }
        }

        while (curNode!=curNode2){
            curNode=curNode.next;
            curNode2=curNode2.next;
        }

        return curNode;

    }

链表是否为回文链表

判断一个链表是否为回文链表
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
  1. 逃避链表,选择数组(面试别这么干)。从数组的两端到中间进行对比。
  2. 利用栈+遍历。整体的思路是将链表的元素全部压栈中,一边出栈,一遍对比即可。在此思路上,我们想一下,是不是只需要压栈链表一半的元素就可以了:那么先遍历一遍链表,获得总长度、一边遍历链表,一边压栈;到达链表的一半时,不再压栈,选择出栈,一边遍历,一边比较。在此思路上,我们想,既然都需要遍历获取总长度,能不能只遍历一次链表呢:可以一边遍历元素压栈,第二次比较时,只比较一半的元素,即只将一半的元素出栈。
  3. 反转链表法。创建一个链表newList,将原始链表的元素值逆序保存到newList中,然后重新遍历两个链表,一遍比较元素的值,只要有一个位置元素不一样,就不是回文链表。基于此,与2中类似,也可以只反转一半:先遍历一遍,获取总长度。然后重新遍历到达一半的位置后不再反转,就开始比较两个链表。
  4. 双指针法(快慢指针)+反转链表。fast一次走两步,slow一次走一步。当fast到尾部时,slow刚好到一半的位置。那么接下来可以从头开开始逆序或者从slow开始逆序一半的元素。
// 利用栈
public static boolean isPalindromeByAllStack(ListNode head) {
        ListNode temp = head;
        Stack<Integer> stack = new Stack();
        //把链表节点的值存放到栈中
        while (temp != null) {
            stack.push(temp.val);
            temp = temp.next;
        }
        //然后再出栈
        while (head != null) {
            if (head.val != stack.pop()) {
                return false;
            }
            head = head.next;
        }
        return true;
    }
// 栈 部分压栈
public static boolean isPalindromeByHalfStack(ListNode head) {
        if(head==null) return true;

        ListNode cur=head;
        Stack<Integer> stack=new Stack<>();
        int len=0;
        while (cur!=null){
            stack.push(cur.val);
            cur=cur.next;
            len++;
        }

        len>>=1; // len/2
        cur=head;
        // 出栈
        while(len-->0){
            if(cur.val !=stack.pop()){
                return false;
            }
            cur=cur.next;
        }
        return true;
    }
// 利用双指针
public static boolean isPalindromeByTwoPoints(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        ListNode slow = head, fast = head;
        ListNode pre = head, prepre = null;
        while (fast != null && fast.next != null) {
            pre = slow;
            slow = slow.next;
            fast = fast.next.next;
            pre.next = prepre;   // 顺序不可改
            prepre = pre;
        }
        if (fast != null) {
            slow = slow.next;
        }
        while (pre != null && slow != null) {
            if (pre.val != slow.val) {
                return false;
            }
            pre = pre.next;
            slow = slow.next;
        }
        return true;
    }

合并有序链表

合并有序链表
将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的。
  1. 新建一张表,分别遍历两个链表,每次都选择最小的结点接到新链表上,最后排完。
  2. 将另外一个链表的结点拆下来,逐个合并到另外一个对应位置上去。
// 方案1
public static ListNode mergeTwoListsMoreSimple(ListNode l1, ListNode l2) {
        ListNode prehead = new ListNode(-1); //虚拟头结点
        ListNode prev = prehead; 
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                prev.next = l1;
                l1 = l1.next;
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
            prev = prev.next;
        }
        // 最多只有一个还未被合并完,直接接上去就行了
        prev.next = l1 == null ? l2 : l1;
        return prehead.next;
    }
合并k个链表

在合并有序链表的基础上,先将前两个合并,然后再讲后面的合并进来。

public static ListNode mergeKLists(ListNode[] lists) {
        ListNode res = null;
        for (ListNode list : lists) {
            res = mergeTwoListsMoreSimple(res, list);
        }
        return res;
    }

寻找中间结点

寻找中间结点
给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
  1. 两个指针slow、fast,一起遍历链表。slow一次走一步,fast一次走两步。当fast走到链表的末尾时,slow必然在中间位置。
public static ListNode middleNode(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

寻找倒数第k个元素

输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例:给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

  1. 快慢双指针。fast向走k+1步,到达第k+1个节点,slow指向链表的第一个结点。此时,指针fast和slow二者之间刚好间隔k个节点,之后同步向后走,当fast走到链表的尾部空节点时,slow指针刚好指向链表的倒数第k个节点。
  2. 栈。
public static 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;
    }

旋转链表

旋转链表
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
  1. 反转链表法。整个链表反转变成{5,4,3,2,1},然后再将前K和N-K两个部分分别反转,也就是分别变成了{4,5}和{1,2,3}
  2. 双指针法。找到倒数K的位置,也就是{1,2,3}和{4,5}两个序列,之后再将两个链表拼接成{4,5,1,2,3} 。再具体的来分析,快指针走k,慢指针在1处,快指针到终点时,慢指针在刚好需要断开的位置。那么快指针指向的结点指向头部,慢指针指向的结点断开与下一个结点的联系(null)。返回慢指针指向结点的下一个结点(恰好断开的位置的下一个)。
// 利用快慢指针
public static ListNode rotateRight(ListNode head, int k) {
        if (head == null || k == 0) {
            return head;
        }
        ListNode temp = head;
        ListNode fast = head;
        ListNode slow = head;
        int len = 0;
        while (head != null) {
            head = head.next;
            len++;
        }
        if (k % len == 0) {
            return temp;
        }
        while ((k % len) > 0) {
            k--;
            fast = fast.next;
        }
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        ListNode res = slow.next;
        slow.next = null;
        fast.next = temp;
        return res;
    }

删除链表特定结点

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

删除节点cur时,必须知道其前驱pre节点和后继next节点,然后让pre.next=next

  1. 虚拟头结点。dummyNode。循环链表找目标元素,遇到目标元素【cur.next=cur.next.next】。注意返回值是dummyNode.next
public static ListNode removeElements(ListNode head, int val) {
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode temp = dummyHead;
        while (temp.next != null) {
            if (temp.next.val == val) {
                temp.next = temp.next.next;
            } else {
                temp = temp.next;
            }
        }
        return dummyHead.next;
    }

删除链表倒数第n个节点

删除链表的倒数第n个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
  1. 栈。将所有的元素压栈,弹出的第N个时候,就是需要删除的。
  2. 双指针。来寻找倒数第n个结点,然后执行删除的操作。
  3. 遍历长度。先遍历一遍,获取总长度len;然后重新遍历,位置[ len-n+1 ]就是我们需要删除的。
// 利用栈
public static ListNode removeNthFromEndByStack(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        Deque<ListNode> stack = new LinkedList<ListNode>();
        ListNode cur = dummy;
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        for (int i = 0; i < n; ++i) {
            stack.pop();
        }
        ListNode prev = stack.peek(); //获取但是不弹出
        prev.next = prev.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

// 双指针
public static ListNode removeNthFromEndByTwoPoints(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode first = head;
        ListNode second = dummy;
        for (int i = 0; i < n; ++i) {
            first = first.next;
        }
        while (first != null) {
            first = first.next;
            second = second.next;
        }
        second.next = second.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

// 遍历求长度
public static ListNode removeNthFromEndByLength(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        int length = getLength(head);
        ListNode cur = dummy;
        for (int i = 1; i < length - n + 1; ++i) {
            cur = cur.next;
        }
        cur.next = cur.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

删除链表中的重复元素

重复元素保留一个
给定一个升序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表
  1. 由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。具体地,我们从指针 cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与cur.next 对应的元素相同,那么我们就将cur.next 从链表中移除;否则说明链表中已经不存在其它与cur 对应的元素相同的节点,因此可以将 cur 指向 cur.next。当遍历完整个链表之后,我们返回链表的头节点即可。另外要注意的是 当我们遍历到链表的最后一个节点
public static ListNode deleteDuplicate(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode cur = head;
        while (cur.next != null) {
            if (cur.val == cur.next.val) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        return head;
    }

重复元素都不保留
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
  1. 当一个都不要时,链表只要直接对cur.next 以及 cur.next.next 两个结点进行比较就行了,这里要注意两个node可能为空,稍加判断就行了。
public static ListNode deleteDuplicates(ListNode head) {
        if (head == null) {
            return head;
        }

        ListNode dummy = new ListNode(0);
        dummy.next = head;
        ListNode cur = dummy;
        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 dummy.next;
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值