面试官常考的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;
}
}
文章目录
一、反转单向链表
题意:输入一个链表,反转链表之后,返回新链表的表头!
这道题,解法有好几种,我们这里就讨论两种思路。
方法一: 头插法与三指针法
头插法:定义pre 、cur和next引用。pre 指向前驱结点,cur指向当前结点,next指向后继结点。
- 首先保存后继结点next。
- 当next保存之后,cur的下一个结点就指向pre。
- 然后pre和cur分别往下走一步。
- 循环往复,由图可知,当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; //将头结点返回去
}
二、反转部分单向链表
这个题就是在反转链表的基础之上进行了改进,换汤不换药。核心代码还是那几行,这道题就是需要一些细节问题。我们先看看反转后的情况:
反转 第2个结点到第4个结点的情况:
由上图可知,我们大概知道思路,该怎么对这个链表着手。
- 首先遍历链表,找到需要反转链表的头结点的上一个结点,对应上图就是
1号结点
,定义变量名为left
- 然后继续往下遍历链表,找到需要反转链表的尾结点 的下一个结点,对应上图就是
5号结点
,定义变量名为right
- 此时声明一个函数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个节点
题意:删除倒数第K个结点。跟另一道题很像“返回倒数第K个结点”。都是一样的意思。
分析:对比上面两幅图,上面那一幅图是倒着走的情况,下面这一副是正着走的情况。 我们会发现下面那一幅图,当fast
刚好走4个结点后,接下来pre
和fast
同时往下走一步,此时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;
}
四、环形链表的约瑟夫环问题
题意:总之就是一句话:给你两个数,一个数是循环链表的长度,另一个数是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;
}
这个约瑟夫环问题,还有一个进阶版的。进阶版的和原题一样,只是测试的数据要多很多。所以一个个去建立循环链表,再去一个个的删除结点。时间效率就很低,感兴趣的朋友可以去看看《程序员代码面试指南》,书中有讲解,如何通过一些规律,来推导出剩下的那个结点,这里我们就不多讲了。
五、判断一个链表是否为回文结构
题意:判断一个单链表是不是回文结构。 回文结构就是:从中间为轴,左右两边对折起来,左右两边每个位置所对应的数值是一样的,比如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;
}
六、将单链表按某值划分左边小、中间等于、右边大
题意:给你一个单链表,和一个数值。根据这个数值将单链表分为小于区、等于区和大于区。
方法一:跟“荷兰国旗问题”一样,我们只需将所有结点放到一个数组里面,然后在数组上做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 命令