链表
1,链表
链表是一种结点相连的数据结构,其中典型的结点构成包括一个存储数据的 data 域和一个存储执行下一结点的指针域 next。双链表则将指针域细化为前驱节点 pre 和后驱节点 next。循环链表则将尾节点和头节点相连。
因此,链表的分类按照实现方式包括单链表、双链表、循环链表。
Java 中的链表:
链表在 Java 中的实现是
2,力扣题型
2.1,203移除链表元素
给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点 。输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
链接:https://leetcode-cn.com/problems/remove-linked-list-elements/
- 递归写法(不需要设置头结点)
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head == null) return null;
//移动到尾部,返回值在压栈的时候表示下一结点,弹栈时表示前一结点
head.next = removeElements(head.next,val);//传的是啥返回的就是啥
//弹栈时head作为当前结点,匹配则返回前一节点;跳过该节点就相当于删除了。
if(head.val == val) return head.next;
//否则,返回前一节点继续
else{
return head;}
}
}
一般方法,将待删除之前一个节点指向待删节点之后一个节点即可。
如果待删除节点是头结点将无法操作。因此增加一个虚拟头结点,统一解决方法。
/**
* Definition for singly-linked list.
* 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; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(-1); //新建一个节点做头节点
dummy.next = head;
ListNode cur = dummy;
while(cur.next != null){
if(cur.next.val == val){
cur.next = cur.next.next;
}else{
cur = cur.next; //否则移动到下一个节点
}
}
//为什么返回的头结点是这个而非cur,因为cur一直在移动
//为什么是dummy.next而非dummy,因为dummy是个虚拟的头节点,要返回自己的
return dummy.next;
}
}
2.2,206反转链表
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。1-> 2 -> 3 -> 4 -> null
null <- 1 <- 2 <- 3 <- 4
迭代法:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null; //前一个结点
ListNode next = null; //下一个结点(临时存储)
ListNode curr = head; //当前结点
while(curr != null){
next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}
递归法:递归单元的执行就是把除了头结点以外的剩余节点进行翻转。
class Solution{
publiC ListNode reverseList(ListNode head){
//递归的停止条件
//我们通常使用head.next判断就可以了,head为null是为了防止链表本身为null的情况
if(head == null || head.next == null)return head;
//压栈,直到移动到最后一个结点
ListNode prev = reverseList(head.next);
head.next.next = head; //弹栈的过程就是把当前结点指向前一个结点的过程
head.next = null; //把前一结点指向null
return prev;
}
}
语句分析: 1-> 2 -> 3 -> 4 -> null
reverseList(3.next)=reverseList(4),其中4.next为null,所以会弹栈跳出;返回的prev即为4.此时head为3。
弹栈后执行:3.next.next=3;3.next = null。即4->3->null
下一步:3->2-null;2->1->null。只压了三次栈,弹了4次栈。
测试demo:
public class ReverseListTest { static int ya = 0; static int tan = 0; public static void main(String[] args) { ListNode head = new ListNode(1); ListNode node2 = new ListNode(2); ListNode node3 = new ListNode(3); ListNode node4 = new ListNode(4); head.next = node2; node2.next = node3; node3.next = node4; node4.next = null; reverseList(head); } public static ListNode reverseList(ListNode head){ ya++; System.out.println("压栈第"+ya+"次"); if(head == null || head.next == null)return head; //压栈,直到移动到最后一个结点 ListNode prev = reverseList(head.next); tan++; System.out.println("弹栈第"+tan+"次"); head.next.next = head; //弹栈的过程就是把当前结点指向前一个结点的过程 head.next = null; //把前一结点指向null return prev; } } class ListNode{ int val; ListNode next; ListNode(int x){ val = x; } }
压栈第1次 压栈第2次 压栈第3次 压栈第4次 弹栈第1次 弹栈第2次 弹栈第3次
2.3,24两两交换链表中的结点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
输入:head = [1,2,3,4] 输出:[2,1,4,3]
思路:
迭代法:更新是一个过程模拟。
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0);//虚拟头结点
dummy.next = head; //加入结点
ListNode temp = dummy;
while(temp.next !=null && temp.next.next !=null){
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2; //temp复位node2
node1.next = node2.next; //node1指向node3
node2.next = node1; //node2指向node1
temp = node1; //前进一步
}
return dummy.next;
}
}
递归法:更简单一点,每次只指向前一个
class Solution {
public ListNode swapPairs(ListNode head) {
// 1,结束条件
if(head == null || head.next == null) return head;
// 2,移动
ListNode next = head.next;
// 注意要间隔一个
head.next = swapPairs(next.next);
// 弹栈执行
next.next = head;
return next;
}
}
2.4,19删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]输入:head = [1], n = 1
输出:[]链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/
思路:该题是双指针的经典应用:首先让快指针先移动n,随后慢指针开始同时移动;当快指针走完时,慢指针就走到了指定位置的前一个位置,直接删除即可。(这里快指针的本质就相当于找到链表的长度了)
移动停止条件:(两种方式)
fIndex !=null
:快指针移动到最后一个的NULL,此时慢指针需要提前到虚拟节点出处开始移动,才能保持同步;虚拟节点的使用统一了在头结点处理的问题。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 双指针
// 因为无法直接获取链表长度,从头往后遍历
ListNode dummy = new ListNode(0,head);
ListNode fIndex = head;
ListNode sIndex = dummy;
if(head == null || head.next == null) return null;
// 先移动n个长度,
for(int i = 0;i<n;i++){
fIndex = fIndex.next;
}
while(fIndex!=null){
fIndex = fIndex.next;
sIndex = sIndex.next;
}
sIndex.next = sIndex.next.next;
return dummy.next;
}
}
fIndex.next != null
,此时快指针少移动一个,移动到了最后位置,那么慢指针就可以从头指针开始,即可保持同步。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 双指针
// 因为无法直接获取链表长度,从头往后遍历
ListNode fIndex = head;
ListNode sIndex = head;
// 先做判空处理
if(head == null || head.next == null) return null;
// 先移动n个长度,
for(int i = 0;i<n;i++){
fIndex = fIndex.next;
}
// 注意:这里就需要考虑删除节点为头结点的情况
// 即删除倒数第n个,移动了n个
if(fIndex == null) return head.next;
while(fIndex.next!=null){
fIndex = fIndex.next;
sIndex = sIndex.next;
}
sIndex.next = sIndex.next.next;
return head;
}
}
(其实,官方题解也给到了我们最为原始的思路,即先算出链表的长度,再从头往后遍历。参考题解)
2.5,面试题 02.07. 链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci
注意这里相交的是开始的某一串节点,而不是一个节点;因此两链表的最后一个节点必须相等;所以两个链表要右对齐。右对齐之后直接遍历即可。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 此种方法的缺点是要考虑到所有输出为null的情况
if (headA == null || headB == null) return null;
int sa=0,sb = 0;
// 算出各自的长度
ListNode tailA = headA,tailB = headB;
while(tailA != null){
tailA = tailA.next;
sa++;
}
while(tailB != null){
tailB = tailB.next;
sb++;
}
//最后一个不一样直接退出
if(tailA!=tailB) return null;
int diff = Math.abs(sa-sb);
if(sa>sb){
//A比B长,A先移动diff位
while(diff-- > 0) headA=headA.next;
}
if(sa<sb){
while(diff-- > 0) headB=headB.next;
}
// 此时就可以直接遍历处理了
while(headA != headB){
headA = headA.next;
headB = headB.next;
}
return headA;
}
2.6,环形链表||
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回
null
。输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
链接:https://leetcode-cn.com/problems/linked-list-cycle-ii/submissions/
方法一,快慢指针:
为什么一定能够相遇?如果同时从头结点出发,且快指针每次走两步,慢指针每次走一步;若存在环,则快慢指针就一定能在环内相遇。当追上slow后,此时一个头节点再和slow同时开始走,那么他们相遇的时候就是环点的位置。推导公式依据快指针走的路是慢指针的2倍。
public class Solution {
public ListNode detectCycle(ListNode head) {
//同时从头结点出发
ListNode fast = head;
ListNode slow = head;
while(fast!=null){
slow = slow.next;
if(fast.next!=null){
fast = fast.next.next;//每次走两步
}else{
return null;
}
if(fast == slow){
//此时prev指针走slow多走的就可以走到环点
ListNode prev = head;
while(prev != slow){
prev = prev.next;
slow = slow.next;
}
return prev;
}
}
return null;
}
}
还可以使用HashSet的不重复特性。
public class Solution {
public ListNode detectCycle(ListNode head) {
// 利用hashset的不重复特性
HashSet<ListNode> hashSet = new HashSet<ListNode>();
ListNode curr = head;
while(curr!=null){
if(!hashSet.contains(curr)){
hashSet.add(curr);
curr = curr.next;
}else{
return curr;
}
}
return null;
}
}
3,总结
递归
迭代
双指针