- 单向项链表
- 结构定义
- 链表中的双指针
- 环形链表问题
- 验证是否是环形链表
- 快慢指针
- 存放到集合中
- 逐个删除
- 先反转在比较
- 关于环形链表,入环点
- 相交链表问题
- 头尾对称寻找法
- 先统计两个链表的长度
- 通过set集合
- 删除链表倒数第N个节点
- 非递归解决
- 双指针求解
- 递归方式解决
- 辅助栈方式解决
- 常见问题及解决方式
- 反转链表的方式
- 迭代
- 递归
- 栈
- 双链表
- 关于递归方式几种方式的区别详解
- 递归的基本模板
- 移除链表元素
- 迭代
- 递归
- 栈
- 奇偶链表问题及解决方式
- 快慢指针
- 回文链表问题及解决方式
- 反转后半部分链表
- 使用栈解决
- 递归方式解决
- 双向链表
- 链表结构定义
- 常见问题及解决方式
- 合并两个有序链表
class MyLinkedList {
class Node{
int val;
Node next;
Node(int val){
this.val=val;
}
}
int size;
Node head;
public MyLinkedList() {
this.size=0;
this.head=null;
}
public int get(int index) {
if(index<0 || index>=size || head==null){
return -1;
}
Node temp=this.head;
for(int i=0;i
temp=temp.next;
}
return temp.val;
}
public void addAtHead(int val) {
Node node=new Node(val);
node.next=this.head;
this.head=node;
size++;
}
public void addAtTail(int val) {
if (size == 0) {
this.head = new Node(val);
head.next = null;
size++;
}else {
Node temp = this.head;
while (temp.next != null) {
temp = temp.next;
}
Node tail = new Node(val);
tail.next = null;
temp.next = tail;
size++;
}
}
public void addAtIndex(int index, int val) {
if (index > this.size) {
return;
}
if (index <= 0) {
addAtHead(val);
return;
}
if (index == this.size) {
addAtTail(val);
return;
}
Node temp = this.head;
for (int i = 0; i < index - 1; i++) {
temp = temp.next;
}
Node insertNode = new Node(val);
insertNode.next = temp.next;
temp.next = insertNode;
size++;
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= this.size) {
return;
}
if (index == 0) {
if (size != 1) {
Node temp = this.head.next;
this.head =temp;
size--;
return;
}else {
this.head = null;
size--;
return;
}
}
Node temp = this.head;
for (int i = 0; i < index - 1; i++) {
temp = temp.next;
}
Node deleteNode = temp.next;
temp.next = deleteNode.next;
size--;
}
}
链表中的双指针
环形链表问题
验证是否是环形链表
快慢指针
判断链表是否有环应该是老生常谈的一个话题了,最简单的一种方式就是快慢指针,慢指针针每次走一步,快指针每次走两步,如果相遇就说明有环,如果有一个为空说明没有环
public boolean hasCycle(ListNode head) {
if (head == null)
return false;
//快慢两个指针
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
//慢指针每次走一步
slow = slow.next;
//快指针每次走两步
fast = fast.next.next;
//如果相遇,说明有环,直接返回true
if (slow == fast)
return true;
}
//否则就是没环
return false;
}
存放到集合中
这题还可以把节点存放到集合set中,每次存放的时候判断当前节点是否存在,如果存在,说明有环,直接返回true,比较容易理解
public boolean hasCycle(ListNode head) {
Set set = new HashSet<>();
while (head != null) {
//如果重复出现说明有环
if (set.contains(head))
return true;
//否则就把当前节点加入到集合中
set.add(head);
head = head.next;
}
return false;
}
逐个删除
一个链表从头节点开始一个个删除,所谓删除就是让他的next指针指向他自己。如果没有环,从头结点一个个删除,最后肯定会删完,如下图所示
如果是环形的,那么有两种情况,一种是o型的,一种是6型的。原理都是一样,我们就看一下o型的
如上图所示,如果删到最后,肯定会出现head=head.next;
public boolean hasCycle(ListNode head) {
//如果head为空,或者他的next指向为空,直接返回false
if (head == null || head.next == null)
return false;
//如果出现head.next = head表示有环
if (head.next == head)
return true;
ListNode nextNode = head.next;
//当前节点的next指向他自己,相当于把它删除了
head.next = head;
//然后递归,查看下一个节点
return hasCycle(nextNode);
}
先反转在比较
public ListNode reverseList(ListNode head) {
//新链表
ListNode newHead = null;
while (head != null) {
//先保存访问的节点的下一个节点,保存起来
//留着下一步访问的
ListNode temp = head.next;
//每次访问的原链表节点都会成为新链表的头结点,
//其实就是把新链表挂到访问的原链表节点的
//后面就行了
head.next = newHead;
//更新新链表
newHead = head;
//重新赋值,继续访问
head = temp;
}
//返回新链表
return newHead;
}
public boolean hasCycle(ListNode head) {
ListNode rev = reverseList(head);
if (head != null && head.next != null && rev == head) {
return true;
}
return false;
}
关于环形链表,入环点
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (slow != null && fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(slow == fast) break;
}
//如果满足下面的条件,说明无环
if(fast == null || fast.next == null) return null;
//下面开始找环的入口节点
//刚才的循环 慢指针走了a + b (a是从开始到入口节点) 假设等于cnt
// 快指针走了 a + b + c + b (b + c = 环),应该等于2 * cnt
//推导出 a = c,所以把慢指针再放到头节点,下一次快慢指针相遇的节点即环的入口节点
slow = head;
while (slow != null && fast != null){
if(slow == fast) break;
slow = slow.next;
fast = fast.next;
}
return fast;
}
相交链表问题
概念:链表中若存在公共节点那么后续节点必然也相同!!
无论两链表是否相交,只需要将链表LA和链表LB的链尾对齐,然后从后向前依次比较节点是否相同即可。
1.从后向前访问的第一节点既不是公共节点则两链表不相交。
2.从后向前访问的公共节点中最后访问的公共节点则为两链表相交的第一个节点。
但单链表想向前遍历节点有点痴人说梦,但是如果将LA+LB和LB+LA的两条轨道拼接起来,此时尾部对齐,从前望后遍历不就能找到公共节点了嘛!(不断的去对方的轨迹中寻找对方的身影,只要二人有交集,就终会相遇!)
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1 = headA, p2 = headB;
//变轨次数
int cnt=0;
while (p1 != null && p2 != null) {
if (p1 == p2) return p1;
p1 = p1.next;
p2 = p2.next;
//p1变轨
if(cnt<2&&p1==null){
p1=headB;
cnt++;
}
//p2变轨
if(cnt<2&&p2==null){
p2=headA;
cnt++;
}
}
return null;
}
先统计两个链表的长度
还可以先统计两个链表的长度,如果两个链表的长度不一样,就让链表长的先走,直到两个链表长度一样,这个时候两个链表再同时每次往后移一步,看节点是否一样,如果有相等的,说明这个相等的节点就是两链表的交点,否则如果走完了还没有找到相等的节点,说明他们没有交点,直接返回null即可,来画个图看一下。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//统计链表A和链表B的长度
int lenA = length(headA), lenB = length(headB);
//如果节点长度不一样,节点多的先走,直到他们的长度一样为止
while (lenA != lenB) {
if (lenA > lenB) {
//如果链表A长,那么链表A先走
headA = headA.next;
lenA--;
} else {
//如果链表B长,那么链表B先走
headB = headB.next;
lenB--;
}
}
//然后开始比较,如果他俩不相等就一直往下走
while (headA != headB) {
headA = headA.next;
headB = headB.next;
}
//走到最后,最终会有两种可能,一种是headA为空,
//也就是说他们俩不相交。还有一种可能就是headA
//不为空,也就是说headA就是他们的交点
return headA;
}
//统计链表的长度
private int length(ListNode node) {
int length = 0;
while (node != null) {
node = node.next;
length++;
}
return length;
}
通过set集合
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//创建集合set
Set set = new HashSet<>();
//先把链表A的结点全部存放到集合set中
while (headA != null) {
set.add(headA);
headA = headA.next;
}
//然后访问链表B的结点,判断集合中是否包含链表B的结点,如果包含就直接返回
while (headB != null) {
if (set.contains(headB))
return headB;
headB = headB.next;
}
//如果集合set不包含链表B的任何一个结点,说明他们没有交点,直接返回null
return null;
}
删除链表倒数第N个节点
非递归解决
这题让删除链表的倒数第n个节点,首先最容易想到的就是先求出链表的长度length,然后就可以找到要删除链表的前一个结点,让他的前一个结点指向要删除结点的下一个结点即可
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = head;
int last = length(head) - n;
//如果last等于0表示删除的是头结点
if (last == 0)
return head.next;
//这里首先要找到要删除链表的前一个结点
for (int i = 0; i < last - 1; i++) {
pre = pre.next;
}
//然后让前一个结点的next指向要删除节点的next
pre.next = pre.next.next;
return head;
}
//求链表的长度
private int length(ListNode head) {
int len = 0;
while (head != null) {
len++;
head = head.next;
}
return len;
}
双指针求解
上面是先计算链表的长度,其实不计算链表的长度也是可以,我们可以使用两个指针,一个指针fast先走n步,然后另一个指针slow从头结点开始,找到要删除结点的前一个结点,这样就可以完成结点的删除了。
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode fast = head;
ListNode slow = head;
//fast移n步,
for (int i = 0; i < n; i++) {
fast = fast.next;
}
//如果fast为空,表示删除的是头结点
if (fast == null)
return head.next;
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
//这里最终slow不是倒数第n个节点,他是倒数第n+1个节点,
//他的下一个结点是倒数第n个节点,所以删除的是他的下一个结点
slow.next = slow.next.next;
return head;
}
递归方式解决
我们知道获取链表的长度除了上面介绍的一种方式以外,还可以写成递归的方式,比如
//求链表的长度
private int length(ListNode head) {
if (head == null)
return 0;
return length(head.next) + 1;
}
上面计算链表长度的递归其实可以把它看做是从后往前计算,当计算的长度是n的时候就表示遍历到了倒数第n个节点了,这里只要求出倒数第n+1个节点,问题就迎刃而解了,来看下代码
public ListNode removeNthFromEnd(ListNode head, int n) {
int pos = length(head, n);
// 说明删除的是头节点
if (pos == n)
return head.next;
return head;
}
// 获取节点所在位置,逆序
public int length(ListNode node, int n) {
if (node == null)
return 0;
int pos = length(node.next, n) + 1;
//获取要删除链表的前一个结点,就可以完成链表的删除
if (pos == n + 1)
node.next = node.next.next;
return pos;
}
辅助栈方式解决
public ListNode removeNthFromEnd(ListNode head, int n) {
// 辅助栈
Deque stack = new LinkedList<>();
ListNode node = new ListNode(0, head);
ListNode tmp = node;
while(tmp != null){
stack.push(tmp);
tmp = tmp.next;
}
for(int i = 0; i < n; i++){
stack.pop();
}
tmp = stack.peek();
tmp.next = tmp.next.next;
return node.next;
}
常见问题及解决方式
反转链表的方式
迭代
public ListNode reverseList(ListNode head){
//1 2 3 4 5
//在链表头前设置一个空指针
ListNode pred = null;
ListNode curr = head;
while(curr!=null){
//将pred与当前curr结点修改指针,同时向后移动,直到curr为空,此时pred到达链表末尾,反转完毕
ListNode next = curr.next;
curr.next = pred;
pred = curr;
curr = next;
}
return pred;
}
递归
public ListNode recursiveLyReverseList(ListNode head){
//退出条件
if(head==null || head.next==null) return head;
//递归调用 回溯 不懂得可以打debug看
ListNode node = recursiveLyReverseList(head.next);
//1 2 3 4 5
//假设除前两结点外其余结点均反转完毕,只需最后一步,反转前两结点
head.next.next = head;
head.next = null;
return node;
}
栈
public ListNode reverseList(ListNode head) {
Stack stack = new Stack<>();
//把链表节点全部摘掉放到栈中
while (head != null) {
stack.push(head);
head = head.next;
}
if (stack.isEmpty())
return null;
ListNode node = stack.pop();
ListNode dummy = node;
//栈中的结点全部出栈,然后重新连成一个新的链表
while (!stack.isEmpty()) {
ListNode tempNode = stack.pop();
node.next = tempNode;
node = node.next;
}
//最后一个结点就是反转前的头结点,一定要让他的next
//等于空,否则会构成环
node.next = null;
return dummy;
}
双链表
public ListNode reverseList(ListNode head) {
//新链表
ListNode newHead = null;
while (head != null) {
//先保存访问的节点的下一个节点,保存起来
//留着下一步访问的
ListNode temp = head.next;
//每次访问的原链表节点都会成为新链表的头结点,
//其实就是把新链表挂到访问的原链表节点的
//后面就行了
head.next = newHead;
//更新新链表
newHead = head;
//重新赋值,继续访问
head = temp;
}
//返回新链表
return newHead;
}
关于递归方式几种方式的区别详解
递归的基本模板
public ListNode reverseList(参数0) {
if (终止条件)
return;
逻辑处理(可能有,也可能没有,具体问题具体分析)
//递归调用
ListNode reverse = reverseList(参数1);
逻辑处理(可能有,也可能没有,具体问题具体分析)
}
终止条件就是链表为空,或者是链表没有尾结点的时候,直接返回
if (head == null || head.next == null)
return head;
递归调用是要从当前节点的下一个结点开始递归。逻辑处理这块是要把当前节点挂到递归之后的链表的末尾,看下代码
public ListNode reverseList(ListNode head) {
//终止条件
if (head == null || head.next == null)
return head;
//保存当前节点的下一个结点
ListNode next = head.next;
//从当前节点的下一个结点开始递归调用
ListNode reverse = reverseList(next);
//reverse是反转之后的链表,因为函数reverseList
// 表示的是对链表的反转,所以反转完之后next肯定
// 是链表reverse的尾结点,然后我们再把当前节点
//head挂到next节点的后面就完成了链表的反转。
next.next = head;
//这里head相当于变成了尾结点,尾结点都是为空的,
//否则会构成环
head.next = null;
return reverse;
}
因为递归调用之后head.next节点就会成为reverse节点的尾结点,我们可以直接让head.next.next = head;,这样代码会更简洁一些,看下代码
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode reverse = reverseList(head.next);
head.next.next = head;
head.next = null;
return reverse;
}
这种递归往下传递的时候基本上没有逻辑处理,当往回反弹的时候才开始处理,也就是从链表的尾端往前开始处理的。我们还可以再来改一下,在链表递归的时候从前往后处理,处理完之后直接返回递归的结果,这就是所谓的尾递归,这种运行效率要比上一种好很多
public ListNode reverseList(ListNode head) {
return reverseListInt(head, null);
}
private ListNode reverseListInt(ListNode head, ListNode newHead) {
if (head == null)
return newHead;
ListNode next = head.next;
head.next = newHead;
return reverseListInt(next, head);
}
尾递归虽然也会不停的压栈,但由于最后返回的是递归函数的值,所以在返回的时候都会一次性出栈,不会一个个出栈这么慢。但如果我们再来改一下,像下面代码这样又会一个个出栈了
public ListNode reverseList(ListNode head) {
return reverseListInt(head, null);
}
private ListNode reverseListInt(ListNode head, ListNode newHead) {
if (head == null)
return newHead;
ListNode next = head.next;
head.next = newHead;
ListNode node = reverseListInt(next, head);
return node;
}
移除链表元素
迭代
public ListNode removeElements(ListNode head, int val) {
//判断是否为空链表,若为空,直接返回
if (head == null) {
return null;
}
//创建头结点的前一个结点,就无需额外考虑头结点的删除操作
ListNode pre = new ListNode(0, head);
//创建临时结点,执行功能
ListNode temp = pre;
//判断当temp的下一结点不为空时才可进入
while (temp.next != null) {
//判断temp的下一结点的值是否为val
if (temp.next.val == val) {
//如果是val,进行删除
temp.next = temp.next.next;
}else {
//如果不是,temp后移
temp = temp.next;
}
}
//因为pre是头结点的前一个结点,所以返回per的下一个结点
return pre.next;
}
递归
public ListNode removeElements(ListNode head, int val) {
if(head == null){
return null;
}
head.next = removeElements(head.next, val);
return head.val == val ? head.next : head;
}
栈
public ListNode removeElements(ListNode head, int val) {
Stack stack = new Stack();
while (head != null) {
if (head.val != val) {
stack.push(head);
}
head = head.next;
}
while (!stack.isEmpty()) {
stack.peek().next = head;
head = stack.pop();
}
return head;
}
奇偶链表问题及解决方式
快慢指针
public static ListNode oddEvenList(ListNode head) {
//如果链表小于3个那么直接返回
if(head==null||head.next==null||head.next.next==null){
return head;
}
//定义第一条指针
ListNode secondhead=head;
//定义第二条指针
ListNode oneHead=head.next;
//定义第一条指针的开始位置
ListNode one=secondhead;
//定义第二条指针开始的位置
ListNode tow=oneHead;
while(secondhead!=null&&secondhead.next!=null&&secondhead.next.next!=null){
//然后这里就是相互的删除中间的元素,也就是逻辑删除
secondhead.next=secondhead.next.next;
secondhead=secondhead.next;
oneHead.next=oneHead.next.next;
oneHead=oneHead.next;
}
secondhead.next=tow;
return one;
}
回文链表问题及解决方式
反转后半部分链表
这题是让判断链表是否是回文链表,所谓的回文链表就是以链表中间为中心点两边对称。我们常见的有判断一个字符串是否是回文字符串,这个比较简单,可以使用两个指针,一个最左边一个最右边,两个指针同时往中间靠,判断所指的字符是否相等。
但这题判断的是链表,因为这里是单向链表,只能从前往后访问,不能从后往前访问,所以使用判断字符串的那种方式是行不通的。但我们可以通过找到链表的中间节点然后把链表后半部分反转(关于链表的反转可以看下432,剑指 Offer-反转链表的3种方式),最后再用后半部分反转的链表和前半部分一个个比较即可。
public boolean isPalindrome(ListNode head) {
ListNode fast = head, slow = head;
//通过快慢指针找到中点
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//如果fast不为空,说明链表的长度是奇数个
if (fast != null) {
slow = slow.next;
}
//反转后半部分链表
slow = reverse(slow);
fast = head;
while (slow != null) {
//然后比较,判断节点值是否相等
if (fast.val != slow.val)
return false;
fast = fast.next;
slow = slow.next;
}
return true;
}
//反转链表
public ListNode reverse(ListNode head) {
ListNode prev = null;
while (head != null) {
ListNode next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
使用栈解决
主要基于栈是先进后出的一种数据结构的特点
public boolean isPalindrome(ListNode head) {
ListNode temp = head;
Stack stack = new Stack();
//把链表节点的值存放到栈中
while (temp != null) {
stack.push(temp.val);
temp = temp.next;
}
//然后再出栈
while (head != null) {
if (head.val != stack.pop()) {
return false;
}
head = head.next;
}
return true;
}
这里相当于链表从前往后全部都比较了一遍,其实我们只需要拿链表的后半部分和前半部分比较即可,没必要全部比较,所以这里可以优化一下
public boolean isPalindrome(ListNode head) {
if (head == null)
return true;
ListNode temp = head;
Stack 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;
}
递归方式解决
如果对链表逆序打印可以这样写(回溯递归)
private void printListNode(ListNode head) {
if (head == null)
return;
printListNode(head.next);
System.out.println(head.val);
}
也就是说最先打印的是链表的尾结点,他是从后往前打印的,如果以后谁再给你说单向链表不能从后往前遍历,你就拿这段代码怼他,哈哈。看到这里是不是有灵感了,我们来对上面的对面进行改造一下
ListNode temp;
public boolean isPalindrome(ListNode head) {
temp = head;
return check(head);
}
private boolean check(ListNode head) {
if (head == null)
return true;
boolean res = check(head.next) && (temp.val == head.val);
temp = temp.next;
return res;
}
双向链表
链表结构定义
class MyLinkedList {
private MutiListNode head;
private MutiListNode tail;
private int size;
public MyLinkedList() {
head = new MutiListNode(0);
tail = new MutiListNode(0);
size = 0;
}
public int get(int index) {
if(index < 0 || index >= size) {
return -1;
}
int i = 0;
MutiListNode tempNode = head.next;
while(tempNode != null) {
if(i == index) {
return tempNode.val;
}
tempNode = tempNode.next;
i ++;
}
return -1;
}
public void addAtHead(int val) {
MutiListNode node = new MutiListNode(val);
if(size == 0) {
head.next = node;
tail.pre = node;
size ++;
return;
}
node.next = head.next;
head.next.pre = node;
head.next = node;
size ++;
}
public void addAtTail(int val) {
if(size == 0) {
addAtHead(val);
return;
}
MutiListNode node = new MutiListNode(val);
tail.pre.next = node;
node.pre = tail.pre;
tail.pre = node;
size ++;
}
public void addAtIndex(int index, int val) {
if(index < 0 || index > size) {
return;
}
if(index == 0) {
addAtHead(val);
return;
}
if(index == size) {
addAtTail(val);
return;
}
int i = 0;
MutiListNode nextNode = null;
MutiListNode preNode = null;
MutiListNode tempNode = head.next;
while (tempNode != null) {
if(index == i) {
nextNode = tempNode;
preNode = tempNode.pre;
break;
}
tempNode = tempNode.next;
i ++;
}
if (preNode != null && nextNode != null) {
MutiListNode node = new MutiListNode(val);
preNode.next = node;
node.pre = preNode;
node.next = nextNode;
nextNode.pre = node;
size ++;
}
}
public void deleteAtIndex(int index) {
if(index < 0 || index >= size) {
return;
}
if(size == 1 && index != 0) {
return;
}
if(size == 1 && index == 0) {
head.next = null;
tail.pre = null;
size --;
return;
}
if(index == 0) {
head.next.next.pre = null;
head.next = head.next.next;
size --;
return;
}
if(index == size-1) {
tail.pre.pre.next = null;
tail.pre = tail.pre.pre;
size --;
return;
}
int i = 0;
MutiListNode tempNode = head.next;
MutiListNode preNode = null;
MutiListNode nextNode = null;
while(tempNode != null) {
if(i == index){
preNode = tempNode.pre;
nextNode = tempNode.next;
break;
}
tempNode = tempNode.next;
i++;
}
if(preNode != null && nextNode != null) {
preNode.next = nextNode;
nextNode.pre = preNode;
size --;
}
}
public class MutiListNode {
int val;
MutiListNode pre;
MutiListNode next;
MutiListNode (int val) {
this.val = val;
}
}
}
常见问题及解决方式
合并两个有序链表
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode pre=new ListNode(),target=null,cur=pre;
while(l1!=null&&l2!=null){
if(l1.val
target=l1;
l1=l1.next;
}else{
target=l2;
l2=l2.next;
}
cur.next=target;
cur=cur.next;
}
cur.next=l1==null?l2:l1;
return pre.next;
}



