面试官常考的15道链表题,你会多少?建议收藏

Posted 飞人01_01

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官常考的15道链表题,你会多少?建议收藏相关的知识,希望对你有一定的参考价值。

面试官常考的15道链表题,你会多少?

链表题,在平时练起来感觉难度还行。但是在面试的过程中,在那种氛围,面试者很容易因为紧张,导致面试表现不好。所以这里总结了一些链表最常见的练习题。反复练习,孰能生巧。希望能帮助到大家!!!

题目在线OJ链接难度
反转单向链表牛客网
反转部分单向链表牛客网
在链表中删除倒数第K个节点牛客网
环形链表的约瑟夫环问题牛客网
判断一个链表是否为回文结构牛客网
将单链表按某值划分左边小、中间等于、右边大于牛客网
单链表的选择排序牛客网
单链表中每K个节点之间进行反转LeetCode
复制一个带随机指针的单链表LeetCode
合并两个有序的链表牛客网
一种怪异节点的删除方式牛客网
按照左右半区的方式重新组合链表牛客网
在单链表中删除重复的节点LeetCode
判断单链表是否有环,若有,返回第一个入环节点LeetCode
//单链表结点
public class ListNode {
    public int val;
    public ListNode next;
    
    public ListNode(int val) {
        this.val = val;
    }
}

本期文章所有源码-GitHub

一、反转单向链表

在线OJ链接

题意:输入一个链表,反转链表之后,返回新链表的表头!

这道题,解法有好几种,我们这里就讨论两种思路。

方法一: 头插法与三指针法

头插法:定义pre 、cur和next引用。pre 指向前驱结点,cur指向当前结点,next指向后继结点。

  1. 首先保存后继结点next。
  2. 当next保存之后,cur的下一个结点就指向pre。
  3. 然后pre和cur分别往下走一步。
  4. 循环往复,由图可知,当cur == null时,循环结束。
//头插法
public ListNode reverseLinkList(ListNode head) {
    if (head == null || head.next == null) { //没有结点,或者只有一个结点的情况
        return head;
    }
    
    ListNode pre = null; //前驱结点
    ListNode cur = head; //当前结点
    ListNode next = null; //后继结点
    while (cur != null) {
        next = cur.next; //首先保存后驱结点
        cur.next = pre; //改变链表的指向
        
        pre = cur; //pre和cur往下走一步
        cur = next;
    }
    return pre;
}

//三指针法---只是在头插法的基础之上改了一下。本质上没有什么区别
public ListNode reverseLinkList(ListNode head) {
    if (head == null || head.next == null) { //没有结点,或者只有一个结点的情况
        return head;
    }
    
    ListNode pre = null; //前驱结点
    ListNode cur = head; //当前结点
    ListNode next = null; //后驱结点
    ListNode newHead = null;
    while (cur != null) {
        next = cur.next; //首先保存后驱结点
        if (newHead == null) { 
            newHead = cur;
        }
        cur.next = pre; //改变链表的指向
        
        pre = cur; //pre和cur往下走一步
        cur = next;
    }
    return pre;
}

方法二:递归

思想:我们只需要一直往下递归,如果cur == null,说明pre这就是原来链表的最后一个结点。我们作为返回值,直接返回即可。递归函数有两个参数ListNode cur, ListNode pre,代表当前结点和上一结点。在找到最后一个结点后,返回的过程中,将cur的下一结点连上pre就行。

public ListNode reverseLinkList(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    return process(head, null); //第一次调用,上一结点就是null
}

public ListNode process(ListNode cur, ListNode pre) {
    if (cur == null) {
        return pre;
    }
    ListNode newHead = process(cur.next, cur); //递归调用下一结点
    cur.next = pre;  //连接pre
    return newHead; //将头结点返回去
}

二、反转部分单向链表

在线OJ链接

这个题就是在反转链表的基础之上进行了改进,换汤不换药。核心代码还是那几行,这道题就是需要一些细节问题。我们先看看反转后的情况:

反转 第2个结点到第4个结点的情况:

由上图可知,我们大概知道思路,该怎么对这个链表着手。

  1. 首先遍历链表,找到需要反转链表的头结点的上一个结点,对应上图就是1号结点,定义变量名为left
  2. 然后继续往下遍历链表,找到需要反转链表的尾结点 的下一个结点,对应上图就是5号结点,定义变量名为right
  3. 此时声明一个函数reverse,将参数(left,开始结点,结束结点,right),转入进去,进行反转,反转后连接left和right即可
public ListNode reversePartList(ListNode head, int L, int R) {
    if (head == null || head.next == null || L > R) { //没有结点,或者只有一个结点的情况
        return head;
    }
    
    ListNode cur = head;
    ListNode pre = null; //临时变量 , pre 是 cur的前驱结点
    
    ListNode left = null; //表头的上一个结点
    ListNode right = null; //尾结点的下一个结点
    
    ListNode start = null; //需要反转链表的表头
    ListNode end = null; //需要反转链表的尾结点
    for (int i = 1; i <= R && cur != null; i++) {
        if (i == L) {
            left = pre; //pre 是 cur的前驱结点
            start = cur;
        }
        if (i == R) {
            end = cur;
            right = cur.next; //反转链表的尾结点  的下一结点
            break;
        } 
        pre = cur;
        cur = cur.next;
    }
    
    reverse(left, start, end, right);
    return left == null? end : head; //有可能反转后,头结点被换了。
}

public void reverse(ListNode left, ListNode start, ListNode end, ListNode right) {
    ListNode next = null;
    ListNode pre = right; //头结点需要连接right,比如上图  2号结点连接5号结点
    while (start != right) {
        next = start.next;
        start.next = pre;
        pre = start;
        start = next;
    }
    //循环结束后,此时pre指向反转链表的尾结点,也就是上图的 4号结点
    if (left != null) {
        left.next = pre; //上图的  1号结点连接4号结点
    }
}

总结:这道题找出4个关键结点,调用reverse函数即可。reverse函数跟第一题的差不多。

三、在链表中删除倒数第K个节点

在线OJ链接

题意:删除倒数第K个结点。跟另一道题很像“返回倒数第K个结点”。都是一样的意思。

分析:对比上面两幅图,上面那一幅图是倒着走的情况,下面这一副是正着走的情况。 我们会发现下面那一幅图,当fast刚好走4个结点后,接下来prefast同时往下走一步,此时pre就是指向了2号结点

​ 也就是说,我们会发现一个规律,假设倒着走K步,我们定义两个引用变量fast和slow,fast先正着走K步后,slow才从头结点开始出发,此时fast和slow一起走,一次走一步,当fast来到尾结点时,此时的slow指向的结点,就是倒数第K个结点。(画草稿图,更容易理解)

public ListNode delBackKNode(ListNode head, int k) {
    if (head == null || k < 1) {
        return head;
    }
    
    ListNode slow = head;
    ListNode fast = head;
    ListNode pre = null; //slow的前驱结点
    for (; fast != null; fast = fast.next) {
        if (--k < 0) {
            pre = slow; //pre紧跟着slow
            slow = slow.next;
        }
    }
    pre.next = slow.next; //C++的朋友,需要自己手动回收ListNode结点的内存
    return head;
}

四、环形链表的约瑟夫环问题

在线OJ链接

题意:总之就是一句话:给你两个数,一个数是循环链表的长度,另一个数是m,每个人报数,报到m的就出局。剩下的接着报数。返回最后剩下的那个结点。

分析: 从头开始遍历,用一个变量记录当前报的数。pre指向前驱结点,cur指向当前结点。循环终止条件就是还剩下一个结点的时候。

public ListNode josephusKill(ListNode head, int k) {
    if (head == null || head.next == head) { //没有结点,或者只有一个结点的情况
        return head;
    }
    
    int count = 1; //计数
    ListNode pre = null;
    ListNode cur = head;
    while (cur != cur.next) { //当自己的next指向自己时,说明只有一个结点了
        if (count++ == k) {
            pre.next = cur.next;
            count = 1; //重置为1
        } else {
            pre = cur;
        }
        cur = cur.next;
    }
    return cur;
}

这个约瑟夫环问题,还有一个进阶版的。进阶版的和原题一样,只是测试的数据要多很多。所以一个个去建立循环链表,再去一个个的删除结点。时间效率就很低,感兴趣的朋友可以去看看《程序员代码面试指南》,书中有讲解,如何通过一些规律,来推导出剩下的那个结点,这里我们就不多讲了。

循环链表的约瑟夫环问题(进阶)

五、判断一个链表是否为回文结构

在线OJ链接

题意:判断一个单链表是不是回文结构。 回文结构就是:从中间为轴,左右两边对折起来,左右两边每个位置所对应的数值是一样的,比如1221,12321就是一个回文数.

方法一:: 判断是不是回文数,我们可以用一个容器先把整个链表的数据装在一起,这里就用,比如链表就是1 -> 2 -> 2 -> 1 ->null;我们压栈后的情况就是这样:

此时我们就已经将整个链表的全部数据压入到栈中,我们都知道,栈是先进后出的,所以在弹出数据的时候,是先弹出栈顶的元素。弹出的元素,我们再去和链表进行比较,如果其中有不相等的,说明这个链表就不是回文结构。

public boolean isPlalindromeList(ListNode head) {
    if (head == null || head.next == null) {
        return true;
    }
    Stack<Integer> stack = new Stack<>(); //栈
    ListNode cur = head;
    while (cur != null) {
        stack.push(cur.val); //压栈
        cur = cur.next;
    }
    
    cur = head;
    while (cur != null) {
        if (cur.val != stack.pop()) {
            return false; //弹出的元素不相等的情况
        }
        cur = cur.next;
    }
    return true;
}

上面的代码,就是运用栈来求解。当然这个方法还可以优化空间复杂度,假设我只压入后半部分的数据,再去和前半部分链表进行比较,也是一样的效果。这里就不多赘述了,自己动手实现一下吧。

方法二: 反转后半部分的链表,优化空间复杂度O(1)

分析:一个向左遍历,一个向右遍历,每次都比较一下,如果有一个不相等。那就不是回文结构。返回结果前,要先把链表反转回来

 //进阶解法,将右边部分,反转链表,指向中间结点
public boolean isPlalindromeList3(ListNode head, int size) { //size,是链表的总长度
    if (head == null || head.next == null) {
        return true;
    }

    boolean res = true;
    ListNode leftStart = head;
    ListNode rightStart = null;
    ListNode cur = head;
    for (int i = 0; i < size / 2; i++) {
        cur = cur.next;
    }

    rightStart =  reverseList(cur); //右半部分的头结点
    cur = rightStart;
    for (int i = 0; i < size / 2; i++) {
        if (cur.val != leftStart.val) {
            res = false;
            break;
        }
        cur = cur.next;
        leftStart = leftStart.next;
    }
    reverseList(rightStart); //恢复链表,不需要接收返回值。本身上一个结点的next域,没被修改
    return res;
}

public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    ListNode pre = null;
    ListNode cur = head;
    ListNode next = null;
    while (cur != null) {
        next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}

六、将单链表按某值划分左边小、中间等于、右边大

在线OJ链接

题意:给你一个单链表,和一个数值。根据这个数值将单链表分为小于区、等于区和大于区。

方法一:跟“荷兰国旗问题”一样,我们只需将所有结点放到一个数组里面,然后在数组上做partition操作。具体的思想,前面的文章八大排序算法 中的快速排序,就是引用了这个思想,可以看一看。

public ListNode listPartiton(ListNode head, int pivot) {
    if (head == null || head.next == null) {
        return head;
    }
    
    ListNode cur = head;
    int count = 0; //计算链表的长度
    while (cur != null) {
        count++;
        cur = cur.next;
    }
    
    ListNode[] arr = new ListNode[count];
    for (int i = 0; i < count; i++) { //将所有结点放入数组
        arr[i] = head;
        head = head.next;
    }
    
    int left = -1; //小于区域的范围
    int right = count; //大于区域的范围
    int index = 0; //用于遍历数组的下标
    while (index < right) { //只要index没有和大于区域相遇,循环就继续
        if (arr[index].val == pivot) {
            index++;  //等于区域,别动,index往后走即可
        } else if (arr[index].val > pivot) {
            swap(arr, index, --right); //切记,这里index还不能动。因为从后面拿前来的数据,还没有判断大小
        } else {
            swap(arr, index++, ++left);
        }
    }
    
    for (int i = 1; i < count; i++) { //连接每个结点
        arr[i - 1].next = arr[i];
    }
    arr[count - 1].next = null; //最后一个结点的next,赋值null
    return arr[0];
}

public void swap(ListNode[] arr, int left, int right) {
    ListNode tmp = arr[left];
    arr[left] = arr[right];
    arr[right] = tmp;
}

方法二: 方法一空间复杂度O(N),可能还不能够得到面试官的青睐,现在再说一种空间复杂度O(1)的解法。

分析:题目是要我分为三个区域,我们就把这三个区域分别看成是3个独立的链表,每个链表都有头尾指针。我们只需要6个引用变量来指向这头尾结点即可。

如图:

public ListNode listPartition2(ListNode head, int pivot) {
    if (head == null || head.next == null) {
        return head;
    }
    
    ListNode sH = null;
    ListNode sT = null; //小于区域
    
    ListNode eH = null;
    ListNode eT = null; //等于区域
    
    ListNode bH = null;
    ListNode bT = null; 256期面试官常考的 21 条 Linux 命令

面试官常考的 21 条 Linux 命令

面试官常考的 21 条 Linux 命令

刷几道链表题,看看自己对指针的把握程度了

刷几道链表题,看看自己对指针的把握程度了

学弟学妹们,别瞎学算法了,跟着师兄来看懂这道链表题!