链表是最基本的数据结构,面试官常常用链表来考察面试者的基本能力,而且链表相关的操作相对而言比较简单,也适合考察写代码的能力。链表的操作也离不开指针,指针又很容易导致出错。综合多方面的原因,链表题目在面试中占据着很重要的地位。
以下内容思路主要参考:算法面试题 | 链表问题总结
推荐一篇文章:基础知识讲的很清楚,Java数据结构与算法之链表
链表的分类:单链表(分带头结点和不带头结点的单链表,就是head里面有没有data的区别)、双向链表、循环链表
重点理解指针的概念
如下代码 ans.next 指向什么?
ans = ListNode(1)
ans.next = head //ans.next 指向取决于最后切断 ans.next 指向的地方在哪,所以ans.next指向head
head = head.next //ans 和 head 被切断联系了
head = head.next
ans = ListNode(1)
head = ans// ans和head共进退
head.next = ListNode(3)
head.next = ListNode(4)
// ans.next 指向什么?ListNode(3)
ans = ListNode(1)
head = ans //head 和 ans共进退
head.next = ListNode(3)
head = ListNode(2) //head 和 ans 的关系就被切断了
head.next = ListNode(4)
居然找到人跟我一样卡在这里了,笑死
* 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; }
* }
//0.找到要插入的位置
temp = 待插入位置的前驱节点.next //1.先用一个临时节点把 待插位置后面的内容先存起来
待插入位置的前驱节点.next = 待插入指针 //2.将新元素插入
待插入指针.next = temp //3.再把后面的元素接到新元素的next
待删除位置的前驱节点.next = 待删除位置的前驱节点.next.next
当前指针 = 头指针
while 当前节点不为空 {
print(当前节点)
当前指针 = 当前指针.next
}
for (ListNode cur = head; cur != null; cur = cur.next) {
print(cur.val)
}
- 如果是遍历链表元素,
while(node!=null)
- 如果是删除某个元素,需要,
while(node.next!=null)
- 需要考虑的仅仅是被改变 next 指针的部分,并且循环之后哪个指针在最后的节点处,就判断谁
//比如快慢指针,输出中间节点,slow和fast的指针都在变,但是fast先指向链表尾巴,所以判断 fast
//同时每个判断next.next的都必须先判断,next,才能保证 奇偶链长 中不会出现空指针异常
while(fast.next!=null && fast.next.next!=null){
slow = slow.next;
fast = fast.next.next;
}
dummy
虚指针HashSet
,删除倒数第k个,利用栈LinkedList
①
NullPointerException
,就是当前节点为空,我们还去操作它的next
;② 输出不了结果,一定是指针移动出了问题
237. 删除链表中的节点 ====面试题 02.03. 删除中间节点
203. 移除链表元素(虚拟头结点)
- 83. 删除排序链表中的重复元素
- 剑指 Offer 18. 删除链表的节点
- 面试题 02.01. 移除重复节点
- 82. 删除排序链表中的重复元素 II
19. 删除链表的倒数第 N 个结点(双指针经典类型)
- 876. 链表的中间结点
- 86. 分隔链表
- 328. 奇偶链表
//237.传入待删除结点,直接将当前节点的值改为next的值,next指向next.next,实现原地更新。
public void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
① 如果删除的节点是中间的节点,则问题似乎非常简单:
- 选择要删除节点的前一个结点
prev
。- 将
prev
的next
设置为要删除结点的next
。② 当要删除的一个或多个节点位于链表的头部时,要另外处理
三种方法:
- 删除头结点时另做考虑(由于头结点没有前一个结点)
- 添加一个虚拟头结点,删除头结点就不用另做考虑
- 递归
- 双指针法
即便是参考别人的代码,一看就看的懂,但其实我们有时候不知道内涵,只要自己闭着眼睛敲一遍,发现了问题,才知道是怎么考虑出来的
// 执行耗时:1 ms,击败了99.79% 的Java用户
// 内存消耗:39.4 MB,击败了49.10% 的Java用户
// 时间复杂度是O(n)。空间复杂度O(1)
public ListNode removeElements(ListNode head, int val) {
//删除值相同的头结点后,可能新的头结点也值相等,用循环解决
//比如输入 [7 7 7 7] 删除7,我一开始是直接用 if,发现有些案例无法通过才知道用while的原因
while(head!=null && head.val==val){
head = head.next;
}
//因为前面是对head的操作,所以极可能最后完了,head为空,所以把判断的过程放在后面
//我本来是吧if放在删除头结点的前面打,结果报错空指针异常,所以才知道为什么判空的要放在后面
if(head==null){
return head;
}
ListNode temp = head;//临时指针
while(temp.next!=null){
if(temp.next.val==val){
temp.next = temp.next.next;
}else{
temp = temp.next;
}
}
return head;
}
添加一个虚拟头结点
//执行耗时:1 ms,击败了99.79% 的Java用户
//内存消耗:39.2 MB,击败了82.52% 的Java用户
//时间复杂度是O(n)。空间复杂度O(1)
public ListNode removeElements(ListNode head, int val){
// 创建虚节点
ListNode dummyNode = new ListNode(val-1);
dummyNode.next = head;
ListNode prev = dummyNode;
while(prev.next!=null){
if(prev.next.val==val){
prev.next = prev.next.next;
}else{
prev = prev.next;
}
}
return dummyNode.next;
}
递归
//时间复杂度是O(n)。递归的方法调用栈深度是n,所以空间复杂度O(n),会超时
public ListNode removeElements(ListNode head, int val){
if(head==null){
return head;
}
// 因为递归函数返回的是已经删除节点之后的头结点
// 所以直接接上在head.next,最后就只剩下判断头结点是否与需要删除的值一致了
head.next = removeElements(head.next,val);
if(head.val==val){
return head.next;
}else{
return head;
}
}
双指针
// 剑指 Offer 18. 删除链表的节点
public ListNode deleteNode(ListNode head, int val){
if(head.val==val){//因为互不相等,如果头指针相等,直接返回
return head.next;
}
//双指针
ListNode pre = head;
ListNode cur = head.next;
while(cur!=null && cur.val!=val){//找元素
pre = cur;
cur = cur.next;
}
if(cur!=null){//找到了,进行删除操作
pre.next = cur.next;
}
return head;//删完了,返回
}
// 面试题 02.01. 移除重复节点
// 法一:借助HashSet的特征
// 移除未排序链表中的重复节点。保留最开始出现的节点,重复的元素不一定连续
public ListNode removeDuplicateNodes(ListNode head) {
if (head == null) {
return head;
}
ListNode temp = head;
HashSet<Integer> set = new HashSet<>();
set.add(head.val);
while(temp.next!=null){
if(set.add(temp.next.val)){//加进去说明不重复
temp = temp.next;
}else{
temp.next = temp.next.next;//原地删除
}
}
return head;
}
// 法二:用空间换时间
// 双重循环,一个定位一个遍历后序,用时间换空间
public ListNode removeDuplicateNodes(ListNode head) {
if(head==null){
return head;
}
ListNode pre = head;
while(pre!=null){
ListNode cur = pre;
while(cur.next!=null){
if(cur.next.val==pre.val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
pre = pre.next;
}
return head;
}
//升序链表,删除链表中所有重复的节点【1 1 1 1 2 3】-->【2 3】
//双指针记录pre 用cur记录相同的数,加虚头节点
public ListNode deleteDuplicates(ListNode head) {
if(head==null){
return head;
}
ListNode dummy = new ListNode(0);//可能删除头结点,所以使用虚节点
dummy.next = head;
ListNode pre = dummy;
ListNode cur = dummy.next;
while(cur!=null && cur.next!=null){//画图最好理解
if(cur.val==cur.next.val ){
//如果有奇数个相同的值,就删不完,所以必须用while循环
while(cur!=null && cur.next!=null && cur.val==cur.next.val ){
cur = cur.next;//找到最后一个相等的数
}
pre.next = cur.next;
cur = pre.next;
}else{
pre = cur;
cur = cur.next;
}
}
return dummy.next;
}
// 删除链表的倒数第 n 个结点,并且返回链表的头结点
// 双指针
public ListNode removeNthFromEnd(ListNode head, int k){
if(head==null) return head;
// 可能会删除头结点
ListNode dummy = new ListNode(0,head);
ListNode pre = dummy.next;
for (int i = 0; i < k; i++) {
pre = pre.next;
}
ListNode cur = dummy;
while(pre!=null){
cur = cur.next;
pre = pre.next;
}
cur.next = cur.next.next;
return dummy.next;
}
// 另外一个方法,利用栈的先进后出特点,效率会更低
// 执行耗时:1 ms,击败了19.42% 的Java用户
// 内存消耗:37.7 MB,击败了5.02% 的Java用户
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null){
return head;
}
ListNode dummy = new ListNode(0,head);
ListNode temp = dummy;
LinkedList<ListNode> stack = new LinkedList<>();
while(temp!=null){
stack.push(temp);
temp = temp.next;
}
for (int i = 0; i < n; i++) {
stack.pop();
}
ListNode pre = stack.peek();
pre.next = pre.next.next;
return dummy.next;
}
//执行耗时:0 ms,击败了100.00% 的Java用户
//内存消耗:35.7 MB,击败了68.38% 的Java用户
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
//如果不加fast != null,链表元素个数为偶数时会报空指针异常
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
两个临时链表
// 给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
// 另外创建一个链表,遍历原来的链表,删除小于的接上去。可能删除头结点
public ListNode partition(ListNode head, int x) {
ListNode small = new ListNode(0);//可能会动头结点,所以需要虚节点
ListNode smallHead = small;//要记住头结点,所以需要另外设置Head
ListNode large = new ListNode(0);
ListNode largeHead = large;
while(head!=null){
if(head.val<x){
small.next = head;
small = small.next;
}else{
large.next = head;
large = large.next;
}
head = head.next;
}
large.next = null;//再拼接两个链表,尾巴指向null
small.next = largeHead.next;
return smallHead.next;
}
两个临时链表的变形
// 给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。
// 请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
// 法一,利用额外空间
public ListNode oddEvenList(ListNode head) {
if(head==null){
return head;
}
ListNode odd = new ListNode(0);
ListNode oddHead = odd;
ListNode even = new ListNode(0);
ListNode evenHead = even;
int count = 1;
while(head!=null){
if(count%2==1){//奇数
odd.next = head;
odd = odd.next;
}else{
even.next = head;
even = even.next;
}
head = head.next;
count++;
}
even.next = null;
odd.next = evenHead.next;
return oddHead.next;
}
直接双指针前后遍历奇数偶数
// 不需要额外空间,双指针操作
public ListNode oddEvenList(ListNode head) {
if(head==null){
return head;
}
ListNode odd = head;
ListNode even = head.next;
ListNode evenHead = even;
while(even!=null && even.next!=null){
odd.next = even.next;//先把奇数连起来
odd = odd.next;//移动奇数的指针
even.next = odd.next;//此时加偶数
even = even.next;//移动偶数的指针
}
odd.next = evenHead;
return head;
}
反转的这些操作尤其需要记忆,要每天多写几遍才可以。
206. 反转链表====剑指 Offer 24. 反转链表
92. 反转链表 II
234. 回文链表====面试题 02.06. 回文链表
25. K 个一组翻转链表
双指针法迭代
//给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
public ListNode reverseList(ListNode head) {
if(head==null){
return head;
}
ListNode pre = null;
ListNode cur = head;
while(cur!=null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
递归法:可参考文章:labuladong:递归反转链表的一部分
- 使用递归的3个条件
- 大问题可以拆解成两个子问题
- 子问题的求解方式和大问题一样
- 存在最小子问题
// 递归法
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null){
return head;
}
//递归最重要的是明确递归函数的定义。
//reverseList代表”「以 head 为起点」的链表反转,并返回反转之后的头结点“
//所以last表示 head.next后面一整段反转之后的头结点。所以最后return last
ListNode last = reverseList(head.next);
//重点理解,此时head.next指向的已经是反转部分的尾巴,也就是图中的2
head.next.next = head;
head.next = null;//head指向null,表示此时head已经是尾巴了。
return last;
}
① 穿针引线法,三个指针
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mv8Avw5Y-1633245545523)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20210920172349817.png)]
node = cur.next;//cur表示当前操作的指针,node保存后面的顺序
cur.next = node.next;
node.next = pre.next;
pre.next = node;
方法一:迭代
// 反转位置left到位置right中间的部分,其余部分不变,
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(-1,head);
//迭代法,先找到起点
ListNode pre = dummy;
for (int i = 0; i < left-1; i++) {
pre = pre.next;//来到 left 节点的前一个节点
}
ListNode cur = pre.next;//cur是真正反转的指针
ListNode node;//node的保存cur.next 的临时指针
for (int i = 0; i < right - left; i++) {
node = cur.next;//保存后面的顺序
cur.next = node.next;//穿针引线,其实很有规律
node.next = pre.next;
pre.next = node;
}
return dummy.next;
}
方法二,递归
// 递归法反转前n个元素
public ListNode reverseN(ListNode head,int n){
ListNode succssor = null;
if(n==1){
successor = head.next;//把后继记录下来,基线是只有一个元素
return head;
}
// 看递归不要进递归函数里面,而是看函数返回了什么结果
ListNode last = reverseN(head.next,n-1);
// 整个链表 = head+reverseN(n-1)这一个已经反转的小团体 + succssor后继
// 注意此时head.next指向的是已经反转的小团体的末尾
head.next.next = head;
head.next = succssor;
return last;
}
public ListNode reverseBetween(ListNode head, int left, int right) {
if(left==1){
return reverseN(head,right);
}
head.next = reverseBetween(head.next,left-1,right-1);
return head;
}
// 请判断一个链表是否为回文链表。
// 反转后半段,两头往中间遍历是否相等,返回前复原链表
public boolean isPalindrome(ListNode head) {
// 1.快慢指针找到中间节点
ListNode slow = head;
ListNode fast = head;
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
ListNode point = slow;//存储中间的断点,用于恢复原来的顺序
if(fast!=null){
slow = slow.next;//slow要定位到中间的后一个位置
}
// 2.反转slow后面的元素
ListNode left = head;
ListNode right = reverse(slow);
ListNode q = right;//存储末尾的断点用于恢复原来链表的顺序
// 3.两头往中间遍历,是否相等,因为后半段尾巴是null
while(right!=null){
if(left.val!=right.val){
return false;
}
left = left.next;
right = right.next;
}
point.next = reverse(q);//还原链表
return true;
}
public ListNode reverse(ListNode head){
if(head==null){
return head;
}
ListNode pre = null;
ListNode cur = head;
while(cur!=null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
感觉一点设计这种一段一段的都可以考虑递归等算法,但对我的下意识总是,暴力求解。。。
学习!努力学习!
public ListNode reverseKGroup(ListNode head, int k) {
if(head==null){
return head;
}
// 1.反转前k个元素
ListNode pre = head;
ListNode cur = head;
for (int i = 0; i < k; i++) {
if(cur==null){
return head;//不够长,保持不变
}
cur = cur.next;
}
ListNode newHead = reverse(pre,cur);
// 2.递归反转后面的,并将后续的连接
// pre一直没有动,所以pre变成最后一个元素之后将next连上
pre.next = reverseKGroup(cur,k);
return newHead;
}
// 反转区间[head end)之间的元素
public ListNode reverse(ListNode head,ListNode end){
ListNode pre = null;
ListNode cur = head;
while(cur!=end){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
合并有序链表问题在面试中出现频率 较高
21. 合并两个有序链表
23. 合并K个升序链表
/**
* 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
*/
public class _03_01_mergeList {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 双指针比较两位置大小,新建一链表
ListNode newHead = new ListNode(0);
ListNode head = newHead;
while(l1!=null && l2!=null){
if(l1.val>l2.val){
newHead.next = l2;
l2 = l2.next;
}else{
newHead.next = l1;
l1 = l1.next;
}
newHead = newHead.next;
}
// 只有一个是空的,那么便把另一个直接接上去就可以了
newHead.next = l1==null?l2:l1;
return head.next;
}
// 2.递归法
public ListNode mergeTwoLists2(ListNode l1, ListNode l2) {
// 递归基线是当前数组为空,直接返回
if(l1==null){
return l2;
}
if(l2==null){
return l1;
}
// 判断当前的大小,原地修改
if(l1.val<=l2.val){
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
}
方法一:逐个合并,方法二,递归分治,时间消耗相对少很多
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0){
return null;
}
// 遍历链表数组,每次选择两个链表,进行合并
int i=0;
ListNode head = null;
while(i<lists.length){
head = merge(head,lists[i]);
i++;
}
return head;
}
public ListNode merge(ListNode l1,ListNode l2){
ListNode newHead = new ListNode(0);
ListNode head = newHead;
while(l1!=null && l2!=null){
if(l1.val>l2.val){
newHead.next = l2;
l2 = l2.next;
}else{
newHead.next = l1;
l1 = l1.next;
}
newHead = newHead.next;
}
// 只有一个是空的,那么便把另一个直接接上去就可以了
newHead.next = l1==null?l2:l1;
return head.next;
}
public ListNode mergeKLists(ListNode[] lists) {
return mergeFrom(lists,0,lists.length-1);
}
public ListNode mergeFrom(ListNode[] lists,int left,int right){
if(left==right){
return lists[left];
}
if(left>right){
return null;
}
int mid = left + (right-left)/2;
//merge是合并两个链表的方法,如上
return merge(
mergeFrom(lists,left,mid),
mergeFrom(lists,mid+1,right)
);
}
147. 对链表进行插入排序
148. 排序链表
public ListNode insertionSortList(ListNode head) {
if(head==null){
return head;
}
// 1.会移动头结点,所以用到虚拟头结点
ListNode dummy = new ListNode(0);
dummy.next = head;
// 2.外层循环遍历完链表所有数,遍历[head,lastSort]这段位置找插入
ListNode cur = dummy.next;
ListNode lastSort = head;//维护已排序部分的最后一个位置
while(cur!=null){//cur为遍历的待插入元素
if(lastSort.val<=cur.val){
lastSort = cur;//大,直接后移
}else{
ListNode pre = dummy;//用来遍历已经排序的部分
// 3.从前往后比较,找插入的位置
while(cur.val>pre.next.val){
pre = pre.next;
}
// 4.找到位置进行插入操作
lastSort.next = cur.next;
cur.next = pre.next;
pre.next = cur;
}
// 5.指针后移
cur = lastSort.next;
}
return dummy.next;
}
// 要求时间空间复杂度分别为O(nlogn)和O(1):归并排序
// 递归额外空间:递归调用函数将带来O(logn)的空间复杂度
// 使用递归版归并,会额外用到logn的时间复杂度
public ListNode sortList(ListNode head) {
// 1.base line
if(head==null || head.next==null){
return head;
}
// 2.找中点,偶数找的前面那个中点的位置,奇数找到中点
ListNode slow =head;
ListNode fast = head.next;
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
// 3.将链表分割成两个子链表
ListNode temp = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(temp);
// 4.新建一个链表,对已排序的链表进行归并操作
ListNode newHead = new ListNode(0);
ListNode res = newHead;
while(left!=null && right!=null){
if(left.val<right.val){
newHead.next = left;
left = left.next;
}else{
newHead.next = right;
right = right.next;
}
newHead = newHead.next;
}
newHead.next = left==null?right:left;
return res.next;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ya2BbAvB-1633245545528)(image/算法/e1dbea51f21247a4972c2cb28855609a~tplv-k3u1fbpfcp-watermark.webp?lastModify=1632129814)]
160. 相交链表
141. 环形链表
// 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
// Set里放的是ListNode而不是ListNode.val,比较的是指针地址
// 方法一:哈希表
// 方法一:哈希表法。存的是ListNode,所以相等时代表地址相同,也就是同一个元素
// 即便val相等,在哈希判断是也不会相等,所以可以使用hash表的方法
public ListNode getIntersectionNode2(ListNode headA, ListNode headB) {
if(headA==null|| headB==null){
return null;
}
// 1.先将某一个链表中的元素存到 set 中
HashSet<ListNode> set = new HashSet<>();
ListNode cur = headA;
while(cur!=null){
set.add(cur);
cur = cur.next;
}
// 2.再遍历第二个链表,如果有就直接返回,如果没有继续遍历
ListNode node= headB;
while(node!=null){
if(set.contains(node)){
return node;
}
node = node.next;
}
return null;
}
//方法二:双指针
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA==null || headB==null){
return null;
}
ListNode nodeA = headA;
ListNode nodeB = headB;
while(nodeA!=nodeB){
// 退出的关键是:指向同一个指针(不是值相等),或者都指向null
nodeA = nodeA==null?headB:nodeA.next;
nodeB = nodeB==null?headA:nodeB.next;
}
return nodeA;//如果没有相等的那么nodeA==nodeB==null
}
// 双指针
public boolean hasCycle(ListNode head) {
if(head==null ||head.next==null){
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while(slow!=fast){
// 因为快指针在前面,所以只要判断快指针是否达到了队尾就可以
if(fast==null || fast.next==null){
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
// 哈希表,耗时非常慢
public boolean hasCycle(ListNode head) {
if(head==null){
return false;
}
HashSet<ListNode> set = new HashSet<>();
ListNode cur = head;
while(cur!=null){
if(set.contains(cur)){
return true;
}
set.add(cur);
cur = cur.next;
}
return false;
}
//给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
// 方法一是哈希表,方法二双指针
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
// 1.快慢指针找重合点
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
// 2.重合了,这个时候,从头来一个指针遍历
if(fast==slow){
ListNode cur = head;
while(cur!=slow){
cur = cur.next;
slow = slow.next;
}
return slow;
}
}
// 3没有环,返回null
return null;
}