Listcode定义
class ListNode {
public int val;
public ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
1.五种方法解决两个链表的第一个公共子节点问题
原题:输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:
两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。
(1)通过哈希或集合实现
-
Hash
public ListNode findFirstCommonNodeByHashMap(ListNode headA,ListNode headB){ HashMap<ListNode,ListNode> map = new HashMap<>(); while (headA != null){ map.put(headA,headA); headA = headA.getNext(); } while (headB != null){ if (map.containsKey(headB)){ return headB; } headB = headB.getNext(); } return null; }
-
Collection
public ListNode findFirstCommonNodeBySet(ListNode headA,ListNode headB){ Set<ListNode> set = new HashSet<>(); while (headA != null){ set.add(headA); headA = headA.getNext(); } while (headB != null){ if (set.contains(headB)){ return headB; } headB = headB.getNext(); } return null; }
(2)通过栈实现
public ListNode findFirstCommonNodeByStack(ListNode headA,ListNode headB){
Stack<ListNode> stackA = new Stack<>();
Stack<ListNode> stackB = new Stack<>();
while(headA!=null){
stackA.push(headA);
headA=headA.next;
}
while(headB!=null){
stackB.push(headB);
headB=headB.next;
}
ListNode pre = null;
while (stackA.size() > 0 && stackB.size() > 0) {
if (stackA.peek() == stackB.peek()) {
pre = stackA.pop();
stackB.pop();
} else {
break;
}
}
return pre;
}
(3)通过双指针法实现
public ListNode findFirstCommonNodeByDoublePointer(ListNode headA,ListNode headB){
if (headA == null || headB == null) {
return null;
}
ListNode node1 = headA;
ListNode node2 = headB;
while (node1 != node2) {
node1 = node1.next;
node2 = node2.next;
if (node1 != node2) {
if (node1 == null) {
node1 = headB;
}
if (node2 == null) {
node2 = headA;
}
}
}
return node1;
}
(4)通过差和双指针实现
示例图:
public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1==null || pHead2==null){
return null;
}
ListNode current1=pHead1;
ListNode current2=pHead2;
int l1=0,l2=0;
//分别统计两个链表的长度
while(current1!=null){
current1=current1.next;
l1++;
}
while(current2!=null){
current2=current2.next;
l2++;
}
current1=pHead1;
current2=pHead2;
int sub=l1>l2?l1-l2:l2-l1;
//长的先走sub步
if(l1>l2){
int a=0;
while(a<sub){
current1=current1.next;
a++;
}
}
if(l1<l2){
int a=0;
while(a<sub){
current2=current2.next;
a++;
}
}
//同时遍历两个链表
while(current2!=current1){
current2=current2.next;
current1=current1.next;
}
return current1;
}
2.判断链表是否存在回文序列
题目:判断一个链表是否为回文链表
示例1:
输入: 1->2->2->1
输出: true
进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
(1)方法1
将链表元素都赋值到数组中,然后可以从数组两端向中间对比。这种方法会被视为逃避链表,面试不能这么干。
public boolean checkIsPalindromicByList(ListNode head) {
if (head == null) {
return true;
}
List<ListNode> list = new ArrayList<>();
while (head != null) {
list.add(head);
head = head.next;
}
int i = 0;
int length = list.size();
while (i < length - 1) {
if (list.get(i).val != list.get(length - 1 - i).val) {
return false;
}
i++;
}
return true;
}
(2)方法2
将链表元素全部压栈,然后一边出栈,一边重新遍历链表,一边比较两者元素值,只要有一个不相等,那就不是。
public boolean isPalindrome(ListNode head) {
ListNode temp = head;
Stack<Integer> 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;
}
(3)方法3
先遍历第一遍,得到总长度。之后一边遍历链表,一边压栈。到达链表长度一半后就不再压栈,而是一边出栈,一边遍历,一边比较,只要有一个不相等,就不是回文链表。这样可以节省一半的空间。
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
// 计算链表长度
int length = 0;
ListNode p = head;
while (p != null) {
length++;
p = p.next;
}
// 反转链表
ListNode prev = null;
ListNode curr = head;
for (int i = 0; i < length / 2; i++) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
// 判断是否为回文链表
ListNode p1 = head;
ListNode p2 = prev;
while (p2 != null) {
if (p1.val != p2.val) {
return false;
}
p1 = p1.next;
p2 = p2.next;
}
return true;
}
(4)通过快慢指针算法实现
public boolean checkIsPalindromeBySlowFastPointer(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
Stack<Integer> stack = new Stack<>();
while (slow != null) {
stack.push(slow.val);
slow = slow.next;
}
while (!stack.isEmpty()){
if(head.val != stack.pop()){
return false;
}
head = head.next;
}
return true;
}
3.合并有序链表
题目:将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的
(1)暴力解法
public ListNode mergeTwoLists (ListNode list1, ListNode list2) {
ListNode newHead=new ListNode(-1);
ListNode res=newHead;
while(list1!=null||list2!=null){
//情况1:都不为空的情况
if(list1!=null&&list2!=null){
if(list1.val<list2.val){
newHead.next=list1;
list1=list1.next;
}else if(list1.val>list2.val){
newHead.next=list2;
list2=list2.next;
}else{ //相等的情况,分别接两个链
newHead.next=list2;
list2=list2.next;
newHead=newHead.next;
newHead.next=list1;
list1=list1.next;
}
newHead=newHead.next;
//情况2:假如还有链表一个不为空
}else if(list1!=null&&list2==null){
newHead.next=list1;
list1=list1.next;
newHead=newHead.next;
}else if(list1==null&&list2!=null){
newHead.next=list2;
list2=list2.next;
newHead=newHead.next;
}
}
return res.next;
}
优化代码:第一个while只处理两个list 都不为空的情况,之后单独写while分别处理list1或者list2不为null的情况
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newHead = new ListNode(-1);
ListNode res = newHead;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
newHead.next = list1;
list1 = list1.next;
} else if (list1.val > list2.val) {
newHead.next = list2;
list2 = list2.next;
} else { //相等的情况,分别接两个链
newHead.next = list2;
list2 = list2.next;
newHead = newHead.next;
newHead.next = list1;
list1 = list1.next;
}
newHead = newHead.next;
//情况2:假如还有链表一个不为空
}
while (list1 != null && list2 == null) {
newHead.next = list1;
list1 = list1.next;
newHead = newHead.next;
}
while (list1 == null && list2 != null) {
newHead.next = list2;
list2 = list2.next;
newHead = newHead.next;
}
return res.next;
}
继续优化代码
进一步分析,我们发现两个继续优化的点,一个是上面第一个大while里有三种情况,我们可以将其合并成两个,如果两个链表存在相同元素,第一次出现时使用if (l1.val <= l2.val)来处理,后面一次则会被else处理掉,什么意思呢?我们看一个序列。
假如list1为{1, 5, 8, 12},list2为{2, 5, 9, 13},此时都有一个node(5)。当两个链表都到5的位置时,出现了list1.val == list2.val,此时list1中的node(5)会被合并进来。然后list1继续向前走到了node(8),此时list2还是node(5),因此就会执行else中的代码块。这样就可以将第一个while的代码从三种变成两种,精简了很多。
第二个优化是后面两个小的while循环,这两个while最多只有一个会执行,而且由于链表只要将链表头接好,后面的自然就接上了,因此循环都不用写,也就是这样:
public ListNode mergeTwoLists2(ListNode list1, ListNode list2) {
ListNode newHead = new ListNode(-1);
ListNode res = newHead;
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
newHead.next = list1;
list1 = list1.next;
} else {
newHead.next = list2;
list2 = list2.next;
}
newHead = newHead.next;
}
// 最多只有一个还未被合并完,直接接上去就行了,这是链表合并比数组合并方便的地方
newHead.next = list1 == null ? list2 : list1;
return res.next;
}
(2)合并K个链表
第一种方法: 使用上面合并两个链表的方法,多次合并
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for (ListNode list: lists) {
res = mergeTwoLists(res, list);
}
return res;
}
第二种方法: 使用小根堆维护 k 个链表的头部,每次比较 K个链表的头结点求出最小值,然后poll出来,若该节点的 next 不为空,则继续加入到堆中
public ListNode mergeLists(ListNode[] lists) {
int length = lists.length;
if (length == 0) {
return null;
}
PriorityQueue<ListNode> pq = new PriorityQueue<>(Comparator.comparingInt(o -> o.val));
for (ListNode list : lists) {
if (list!=null){
pq.add(list);
}
}
ListNode res = new ListNode(0);
ListNode cur = res;
while (!pq.isEmpty()){
ListNode tmp = pq.poll();
cur.next = tmp;
cur = cur.next;
if (tmp.next!=null){
pq.add(tmp.next);
}
}
return res.next;
}
(3)牛刀小试
LeetCode1669:给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。请你将 list1 中下标从a到b的节点删除,并将list2 接在被删除节点的位置。
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
ListNode pre1 = list1, post1 = list1, post2 = list2;
int i = 0, j = 0;
while(pre1 != null && post1 != null && j < b){
if(i != a - 1){
pre1 = pre1.next;
i++;
}
if(j != b){
post1 = post1.next;
j++;
}
}
post1 = post1.next;
//寻找list2的尾节点
while(post2.next != null){
post2 = post2.next;
}
//链1尾接链2头,链2尾接链1后半部分的头
pre1.next = list2;
post2.next = post1;
return list1;
}
1.变化1:定义list1的[a,b]区间为list3,将list3和list2按照升序合并成一个链表。
public LinkNode mergeInBetween(LinkNode list1, int a, int b, LinkNode list2) {
LinkNode pre1 = list1, post2 = list2;
LinkNode tmp = new LinkNode(-1, null);
LinkNode list3;
int i = 1;
PriorityQueue<LinkNode> pq = new PriorityQueue<>(Comparator.comparingInt(LinkNode::getValue));
while (pre1 != null) {
if (i < a || i > b) {
pq.add(pre1);
}
pre1 = pre1.getNext();
i++;
}
while (post2 != null) {
pq.add(post2);
post2 = post2.getNext();
}
list3 = tmp;
while (!pq.isEmpty()) {
tmp.setNext(pq.poll());
tmp = tmp.getNext();
}
return list3;
}
2.变化2:list2也将区间[a,b]的元素删掉,然后将list1和list2合并成一个链表
public LinkNode mergeInBetween(LinkNode list1, int a, int b, LinkNode list2) {
LinkNode pre1 = list1, post2 = list2;
LinkNode tmp = new LinkNode(-1, null);
LinkNode list3;
int i = 1;
PriorityQueue<LinkNode> pq = new PriorityQueue<>(Comparator.comparingInt(LinkNode::getValue));
while (pre1 != null) {
if (i < a || i > b) {
pq.add(pre1);
}
pre1 = pre1.getNext();
i++;
}
i = 1;
while (post2 != null) {
if (i < a || i > b) {
pq.add(post2);
}
post2 = post2.getNext();
i++;
}
list3 = tmp;
while (!pq.isEmpty()) {
tmp.setNext(pq.poll());
tmp = tmp.getNext();
}
return list3;
}
4.寻找中间结点
原题:LeetCode876 给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例1
输入:[1,2,3,4,5]
输出:此列表中的结点 3
示例2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4
这个问题用经典的快慢指针可以轻松搞定,用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。
public ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
5.寻找倒数第K个元素
输入一个链表,输出该链表中倒数第k个节点。本题从1开始计数,即链表的尾节点是倒数第1个节点。
示例
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
这里可以使用快慢双指针,先将fast 向后遍历到第 k+1 个节点, slow仍然指向链表的第一个节点,此时指针fast 与slow 二者之间刚好间隔 k 个节点。之后两个指针同步向后走,当 fast 走到链表的尾部空节点时,slow 指针刚好指向链表的倒数第k个节点。
这里需要特别注意的是链表的长度可能小于k,寻找k位置的时候必须判断fast是否为null
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && k > 0) {
fast = fast.next;
k--;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
6.旋转链表
7.删除链表特定节点
题目:LeetCode 203:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点
实现步骤
- 1.创建一个虚拟链表头dummyHead,使其next指向head。
- 2.开始循环链表寻找目标元素,注意这里是通过cur.next.val来判断的。
- 3.如果找到目标元素,就使用cur.next = cur.next.next;来删除。
- 4.注意最后返回的时候要用dummyHead.next,而不是dummyHead。
public LinkNode deleteNodeByVal(LinkNode head, int val) {
if (head == null) {
return head;
}
LinkNode cur = new LinkNode(-1, head);
LinkNode res = cur;
while (cur != null && cur.getNext() != null) {
if (cur.getNext().getValue() == val) {
cur.setNext(head.getNext().getNext());
} else {
cur = cur.getNext();
}
}
return res;
}
8.删除链表倒数第K个结点
题目:删除链表的倒数第n个结点,并且返回链表的头结点。
示例1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
/**
* 首先从头节点开始对链表进行一次遍历,得到链表的长度 L。随后再从头节点开始对链表进行一次遍历,当遍历到第L−n+1个节点时,则是删除的节点。
*/
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next=head;
int length = getLength(head);
ListNode cur = dummy;
for (int i = 1; i < length - n + 1; ++i) {
cur = cur.next;
}
cur.next = cur.next.next;
ListNode ans = dummy.next;
return ans;
}
public int getLength(ListNode head) {
int length = 0;
while (head != null) {
++length;
head = head.next;
}
return length;
}
进阶:尝试使用一趟扫描实现
双指针查找:
public LinkNode removeNthFromEnd2(LinkNode head, int n) {
LinkNode res = head;
LinkNode slow = head;
LinkNode fast = head;
for (int i = 0; i < n; i++) {
fast = fast.getNext();
}
while (fast != null) {
slow = slow.getNext();
fast = fast.getNext();
}
slow.setNext(slow.getNext().getNext());
return res;
}
9.删除链表中的重复元素
(1)重复元素保留一个
存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素只出现一次 。返回同样按升序排列的结果链表。
示例1:
输入:head = [1,1,2,3,3]
输出:[1,2,3]
public LinkNode deleteDuplicates(LinkNode headNode){
if (headNode == null){
return headNode;
}
LinkNode cur = headNode;
while (cur.getNext() != null){
if (cur.getValue() == cur.getNext().getValue()) {
cur.setNext(cur.getNext().getNext());
} else {
cur = cur.getNext();
}
}
return headNode;
}
(2)重复元素都不要
示例1:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode dummy = new ListNode(0, head);
ListNode cur = dummy;
while (cur.next != null && cur.next.next != null) {
if (cur.next.val == cur.next.next.val) {
int x = cur.next.val;
while (cur.next != null && cur.next.val == x) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return dummy.next;
}