编程导航算法通关村第1关 | 链表高频面试算法题

本文用到的链表的定义

class ListNode {
    public int val;
    public ListNode next;

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

题目一:两个链表第一个公共子节点

这是一道经典的链表问题,剑指offer52 先看一下题目:输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:
在这里插入图片描述

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

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

没有思路时该怎么解题?

将常用数据结构和常用算法思想都想一遍,看看哪些能解决问题。
常用的数据结构有数组、链表、队、栈、Hash、集合、树、堆。常用的算法思想有查找、排序、双指针、递归、迭代、分治、贪心、回溯和动态规划等等。

hash和集合

先将一个链表元素全部存到Map里,然后一边遍历第二个链表,一边检测Hash中是否存在当前结点,如果有交点,那么一定能检测出来。 对于本题,如果使用集合更适合,而且代码也更简洁。

public 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;
}

使用栈

这里需要使用两个栈,分别将两个链表的结点入两个栈,然后分别出栈,如果相等就继续出栈,一直找到最晚出栈的那一组。

在这里插入图片描述

import java.util.Stack;
public 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 ListNode findFirstCommonNode(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;
}

差和双指针

首先第一轮 遍历两个链表 分别获取其长度La、Lb,第二轮 先让长的链表先走 |La-Lb| 让两个未走的长度相同,再一个一个对比。

在这里插入图片描述

public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    if(pHead1==null || pHead2==null){
        return null;
    }
    ListNode current1=pHead1;
    ListNode current2=pHead2;
    int l1=0,l2=0;
    //分别统计两个链表的长度
    while(current1!=null){
        current1=current1.next;
        l1++;
    }

    while(current2!=null){
        current2=current2.next;
        l2++;
    }
    current1=pHead1;
    current2=pHead2;
    int sub=l1>l2?l1-l2:l2-l1;
    //长的先走sub步
    if(l1>l2){
        int a=0;
        while(a<sub){
            current1=current1.next;
            a++;
        }   
    }

    if(l1<l2){
        int a=0;
        while(a<sub){
            current2=current2.next;
            a++;
        }   
    }
    //同时遍历两个链表
    while(current2!=current1){
        current2=current2.next;
        current1=current1.next;
    } 

    return current1;
}

题目二:判断链表是否为回文序列

LeetCode234

在这里插入图片描述

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

与上一个题相同,先考虑常见的数据结构和算法思想,是否可以解决。

  1. 数组:将链表中元素取出,放入数组中,使用下标从两头向中间遍历。
  2. 栈:第一轮遍历,将链表中节点按顺序全部压入栈,第二轮遍历,出栈并与链表对比。
  3. 优化2,先遍历第一遍,得到总长度。之后一边遍历链表,一边压栈。到达链表长度一半后就不再压栈,而是一边出栈,一边遍历,一边比较,只要有一个不相等,就不是回文链表。这样可以节省一半的空间。
  4. 优化3,第一轮遍历,压入栈时顺便记录长度,第二轮出栈对比只对比一半。
  5. 链表:创建一个新的链表,将原链表中的节点按顺序存入新链表,这样新链表就是逆向的原链表(和栈差不多),之后再一个一个对比。
  6. 优化5,第一轮遍历长度,第二轮将原链表中的节点按顺序存入新链表到一半时,就可以对比了。
  7. 优化6,使用双指针中的快慢指针,快指针fast一次走两步,慢指针slow一次走一步,当fast到达链表尾部时,slow到达链表中间,接下来可以从头开始逆序一半的元素,或者从slow开始逆序一半的元素,再对比。
    /**
     * 3:只将一半的数据压栈
     *
     * @param head
     * @return
     */
    public static boolean isPalindromeByHalfStack(ListNode head) {
        if (head == null)
            return true;
        ListNode temp = head;
        Stack<Integer> stack = new Stack();
        //链表的长度
        int len = 0;
        //把链表节点的值存放到栈中
        while (temp != null) {
            stack.push(temp.val);
            temp = temp.next;
            len++;
        }
        //len长度除以2
        len >>= 1;
        //然后再出栈
        while (len-- >= 0) {
            if (head.val != stack.pop())
                return false;
            head = head.next;
        }
        return true;
    }

    /**
     * 7:通过双指针的快慢指针方式来判断
     *
     * @param head
     * @return
     */
    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;
    }

题目三:合并两个有序链表

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

创建一个新链表,两个链表一个一个取出节点对比小的节点,放入新链表中。

public ListNode mergeTwoLists (ListNode list1, ListNode list2) {
     ListNode newHead=new ListNode(-1);
     ListNode res=newHead;
     while(list1!=null||list2!=null){ 
         //情况1:都不为空的情况
         if(list1!=null&&list2!=null){
             if(list1.val<list2.val){
                 newHead.next=list1;
                 list1=list1.next;
             }else if(list1.val>list2.val){
                 newHead.next=list2;
                 list2=list2.next;
             }else{ //相等的情况,分别接两个链
                 newHead.next=list2;
                 list2=list2.next;
                 newHead=newHead.next;
                 newHead.next=list1;
                 list1=list1.next;
             }
             newHead=newHead.next;
          //情况2:假如还有链表一个不为空
         }else if(list1!=null&&list2==null){
             newHead.next=list1;
             list1=list1.next;
             newHead=newHead.next;
         }else if(list1==null&&list2!=null){
             newHead.next=list2;
             list2=list2.next;
             newHead=newHead.next;
         }
     }
     return res.next;
 }

优化

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    ListNode prehead = new ListNode(-1);
    ListNode prev = prehead;
    while (list1 != null && list2 != null) {
        if (list1.val <= list2.val) {
            prev.next = list1;
            list1 = list1.next;
        } else {
            prev.next = list2;
            list2 = list2.next;
        }
        prev = prev.next;
    }
    // 最多只有一个还未被合并完,直接接上去就行了,这是链表合并比数组合并方便的地方
    prev.next = list1 == null ? list2 : list1;
    return prehead.next;
}

题目四:删除特定结点

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

示例1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

我们删除某个节点需要知道该节点的前一个结点preNode使其preNode.next = preNode.next.next

所以该题我们可以再创建一个节点dummyHead使其next指向head,当从dummyHead节点开始遍历当其下一个节点的val与要删除的val相同时,就可以删掉下一个节点了。

在这里插入图片描述

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

题目五:删除倒数第n个结点

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

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

第一种方法:先遍历一遍链表,找到链表总长度L,然后重新遍历,位置L-N+1的元素就是我们要删的。
第二种方法:将元素全部压栈,然后弹出第N个的时候就是我们要删除的节点。
第三种方法:使用双指针来寻找倒数第K个节点,定义两个指针first,second,firest先走k步既走到位置为k+1的节点,此时firest和second相差k,二者同时继续遍历,当frest走到链表尾部的null时second正好是倒数第k个节点。

在这里插入图片描述

public ListNode removeNthFromEnd(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;
}

题目六:删除重复元素

LeetCode82:这个题目的要求是重复的元素都不要了

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

    ListNode dummy = new ListNode(0, 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. 数据结构和算法面试官可能会问你于常见数据结构(如数组、链表、栈、队列、树、图)和算法(如排序、查找、递归、动态规划)的问。他们可能会要求你解释算法的时间复杂度和空间复杂度,并要求你分析和优化算法。 2. 编程面试官可能会要求你在面试过程中编写代码来解决特定的问。这些问可能涉及字符串处理、数组操作、递归等。你需要展示你的编程能力和解决问的能力。 3. 系统设计:面试官可能会要求你设计一个复杂的系统,例如一个搜索引擎、社交媒体平台或分布式系统。你需要考虑系统的架构、数据存储、性能优化等方面。 4. 算法优化:面试官可能会要求你优化给定的算法或解决特定问算法。你需要展示你的思考过程和优化技巧。 5. 算法思维:面试官可能会要求你解决一些算法思维,例如找出数组中的重复元素、判断链表是否有环等。这些问需要你具备良好的逻辑思维和问解决能力。 请注意,以上只是一些可能出现的问类型,具体的面试题目可能因公司和职位而异。在准备面试时,建议你熟悉常见的数据结构和算法,并进行练习和复习。还可以参考一些面试题目整理的资料,如引用和引用所提供的内容。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值