青铜🍄
两个链表,找公共子节点
方法:可以将两个链表存到某个数据结构中,如hashMap、hashSet、栈中,然后依次比较
1.hashSet 辅助查找
将其中一个链表存到 hashSet中,利用hashSet.contains( )方法 来比较
/**
* 通过 hashSet 辅助查找
* @param headA
* @param headB
* @return
*/
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet();
// 将链表A 放进 hashSet中
while (headA != null) {
set.add(headA);
headA = headA.next;
}
// 遍历链表B,并检测 set是否存在当前结点
while (headB != null) {
if (set.contains(headB)) {
return headB;
}
headB = headB.next;
}
return null;
}
2.hashMap 辅助查找
new 一个hashMap,HashMap<ListNode, Integer> hashMap = new HashMap<>();
key值为结点,value为null即可。
将其中一个链表存到 hashMap中,利用hashMap.containsKey( )方法 来比较
/**
* 通过 HashMap 辅助查找
* @param head1
* @param head2
* @return
*/
public static ListNode findFirstCommonNodeByMap(ListNode head1, ListNode head2) {
HashMap<ListNode, Integer> hashMap = new HashMap<>();
while (head1 != null) {
hashMap.put(head1,null);
head1 = head1.next;
}
while (head2 != null) {
if (hashMap.containsKey(head2)) {
return head2;
}
head2 = head2.next;
}
return null;
}
3.通过栈, 辅助查找
栈是先入后出。所以先出的结点是相同的结点。
/**
* 通过栈 辅助查找
* @param head1
* @param head2
* @return
*/
public static ListNode findFirstCommonNodeByStack(ListNode head1,ListNode head2) {
Stack<ListNode> stack1 = new Stack();
Stack<ListNode> stack2 = new Stack();
while (head1 != null) {
stack1.push(head1);
head1 = head1.next;
}
while (head2 != null) {
stack2.push(head2);
head2 = head2.next;
}
ListNode preNode = null;
while (stack1.size() > 0 && stack2.size() > 0) {
// 栈是先入后出。所以先出的结点是相同的结点
if (stack1.peek() == stack2.peek()) {
preNode = stack1.pop();
stack2.pop();
} else {
break;
}
}
return preNode;
}
🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄🍄
白银🍄
1.两个链表第一个公共子节点
1.1 蛮力法
在第一链表上顺序遍历每个节点,每遍历到一个节点,就在第二个链表上顺序遍历每个节点。如果在第二个链表上有一个节点和第一个链表上的节点一样,则说明两个链表在这个节点上重合,时间复杂度高,排除!!!
1.2 HashMap辅助查找
将一个链表全部存到HashMap里,然后再遍历第二个,如果有交点,那么一定能在访问到某个元素的时候检测出来;这里存放的是HashMap的键,因为HashMap的键是唯一的,不允许重复!!!
/**
* 通过 hashMap 辅助查找
* @param head1
* @param head2
* @return
*/
public static ListNode findFirstCommonNodeByMap(ListNode head1, ListNode head2) {
HashMap<ListNode, Integer> hashMap = new HashMap<>();
while (head1 != null) {
hashMap.put(head1,null);
head1 = head1.next;
}
while (head2 != null) {
if (hashMap.containsKey(head2)) {
return head2;
}
head2 = head2.next;
}
return null;
}
1.3 HashSet辅助查找
可以用HashMap,也可以用 HashSet试试!
/**
* 通过 hashSet 辅助查找
* @param headA
* @param headB
* @return
*/
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet();
// 将链表A 放进 hashSet中
while (headA != null) {
set.add(headA);
headA = headA.next;
}
// 遍历链表B,并检测 set是否存在当前结点
while (headB != null) {
if (set.contains(headB)) {
return headB;
}
headB = headB.next;
}
return null;
}
1.4 使用栈辅助查找
使用2个栈,将2个链表分别入栈,然后分别出栈(此时出栈的都是相同的节点),如果相等就继续出栈,不相等的时候就找到了分界线了
/**
* 通过栈 辅助查找
* @param head1
* @param head2
* @return
*/
public static ListNode findFirstCommonNodeByStack(ListNode head1,ListNode head2) {
Stack<ListNode> stack1 = new Stack();
Stack<ListNode> stack2 = new Stack();
while (head1 != null) {
stack1.push(head1);
head1 = head1.next;
}
while (head2 != null) {
stack2.push(head2);
head2 = head2.next;
}
ListNode preNode = null;
while (stack1.size() > 0 && stack2.size() > 0) {
// 栈是先入后出。所以先出的结点是相同的结点
if (stack1.peek() == stack2.peek()) {
preNode = stack1.pop();
stack2.pop();
} else {
break;
}
}
return preNode;
}
1.5 差和双指针
第一次遍历两个链表,算出两个链表的长度以及长链表比短链表多出若干节点?
第二次遍历,长链表先走若干个节点,然后长链表、短链表一起遍历,找到的第一个相同的节点就是它们的第一个公共节点。
/**
* 差和双指针
* @param head1
* @param head2
* @return
*/
public static ListNode findFirstCommonNode_2(ListNode head1, ListNode head2) {
int head1Length = getLength(head1);
int head2Length = getLength(head2);
int sub = head1Length >= head2Length ? head1Length - head2Length : head2Length - head1Length;
if (head1Length > head2Length) {
int k = 0;
while (k < sub) {
k++;
head1 = head1.next;
}
}
if (head2Length > head1Length) {
int k = 0;
while (k < sub) {
k++;
head2 = head2.next;
}
}
// 同时遍历两个链表
while (head1 != head2) {
head1 = head1.next;
head2 = head2.next;
}
return head1;
}
2.判断链表是否为回文序列
2.1 全部压栈
将链表全部入栈,一边出栈,一边与链表比较,如果全部相同,就是回文序列
/**
* 全部压栈
* @param head
* @return
*/
public static boolean isPalindromeByAllStack(ListNode head) {
Stack<Integer> stack = new Stack<>();
ListNode cur = head;
// 全部压栈
while (cur != null) {
stack.push(cur.val);
cur = cur.next;
}
// 全部出栈,并与链表进行比较
while (stack.size() > 0) {
if (head.val != stack.pop()) {
return false;
}
head = head.next;
}
return true;
}
2.2 出栈一半
先遍历第一遍,得到总长度。之后一遍历链表,一遍压栈。当到达链表长度一半的位置之后,就不再压栈,而是一边出栈,一遍遍历,一遍比较,只要有一个不相等,就不是回文链表。
/**
* 将全部数据压栈,只出栈一半的数据并比较
*
* @param head
* @return
*/
public static boolean isPalindromeByHalfStack(ListNode head) {
if (head == null) {
return true;
}
Stack<Integer> stack = new Stack<>();
ListNode cur = head;
while (cur != null) {
stack.push(cur.val);
// 将全部数据压栈
cur = cur.next;
}
// 数据的长度
int size = stack.size();
// 数据长度的一半
int len = size / 2;
while (len-- > 0) {
if (head.val != stack.pop()) {
return false;
}
head = head.next;
}
return true;
}
2.3 快慢指针法
3. 合并有序链表
3.1 合并两个有序链表
解题思路:一,新建一个新链表,然后分别遍历两个链表,将最小节点链接到新链表上,最后排完;二,建一个链表结点拆下来,逐个合并到另一个链表对应的位置上去。
/**
* 方法1:面试时就能写出来的方法
*
* @param list1
* @param list2
* @return
*/
public static ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newHead = new ListNode(-1);
ListNode res = newHead;
while (list1 != null && list2 != null) {
// 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 = list1;
list1 = list1.next;
newHead = newHead.next;
newHead.next = list2;
list2 = list2.next;
}
newHead = newHead.next;
}
while (list1 != null) {
// list1不为null 和 list2为null 的情况下
newHead.next = list1;
list1 = list1.next;
newHead = newHead.next;
}
while (list2 != null) {
// list1为null 和 list2不为null 的情况下
newHead.next = list2;
list2 = list2.next;
newHead = newHead.next;
}
return res.next;
}
方法2:在方法1中,发现两个继续优化的点,一个是上面第一个大while里有三种情况,我们可以将其合并成两个,如果两个链表存在相同元素,第一次出现时使用if(l1.val<= 12.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最多只有一个会执行,而且由于链表只要将链表头接好,后面的自然就接上了。
/**
* 方法2,方法1的优化
* @param list1
* @param list2
* @return
*/
public static ListNode mergeTwoLists2(ListNode list1, ListNode list2) {
ListNode preHead = new ListNode(-1);
ListNode result = preHead;
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
preHead.next = list1;
list1 = list1.next;
} else {
preHead.next = list2;
list2 = list2.next;
}
preHead = preHead.next;
}
preHead.next = list1 == null ? list2 : list1;
return result.next;
}
4. 双指针
双指针就是两个变量,通过双指针来遍历数据结构,可以有效解决许多问题,如排序、查找、合并等。
4.1 寻找链表的中间节点
使用快慢指针,slow 和 fast,slow走一步,fast走两步,fast到达链表结尾,slow则到达链表中间。
public static ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
4.2 返回倒数第k个节点
解题思路:快慢指针,快指针先后遍历到 k+1 个节点,慢指针指向头节点,这样快慢指针之间间隔 k 个节点。之后两个指正同步向后走,快指针走到链表尾部的空节点时,慢指针刚好指向链表的倒数第k个节点。
public static 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.val;
}
4.3 旋转链表
思路:链表向右移动 k 个位置,假设 k = 2,这时候就将链表分为两个部分{1,2,3}和{4,5}。
最终结果是,{4,5}链接{1,2,3},即,4->5->1->2->3。解题思路就是找到这两部分
注意,这里要考虑 k 可能大于链表长度的情况。如果 长度为5,那么 k = 2和 k = 7的情况是一样的,这里可以用取余 %
public static ListNode rotateRight(ListNode head, int k) {
if (head == null || k == 0) {
return head;
}
ListNode temp = head;
ListNode fast = head;
ListNode slow = head;
int len = 0;
// 因为 k 可能 大于链表长度,我们先计算链表长度
while (head != null) {
len++;
head = head.next;
}
// 当 k % len == 0 时,直接返回原链表即可
if (k % len == 0) {
return temp;
}
// 1.快指针先走k步
// 这里使用取模,是为了防止 k > len的情况
// 假设len=5,那么k=2和k=7,效果是一样的
while ((k % len) > 0) {
k--;
fast = fast.next;
}
// 2.快慢指针一起执行
// 当fast到尾结点时,slow刚好在倒数第k个位置上
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
// 将快指针的下一结点指向头节点,将慢指针的下一结点
ListNode res = slow.next;
fast.next = temp;
slow.next = null;
return res;
}
5. 删除元素
5.1 删除特定结点
遍历链表,符合就移除!
/**
* 删除特定值的结点
*
* @param head
* @param val
* @return
*/
public static ListNode removeElements(ListNode head, int val) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode temp = dummyHead;
while (temp.next != null) {
if (temp.next.val == val) {
temp.next = temp.next.next;
} else {
temp = temp.next;
}
}
return dummyHead.next;
}
5.2 删除倒数第 n 个节点
解题思路:找到要删除节点的前一节点,node.next = node.next.next
方法1:双指针
通过快慢指针,拿到{3,4,5},{4}为需要删除的倒数第2个节点,通过slow.next = slow.next.next删除{4}
/**
* 方法1:通过双指针
*
* @param head
* @param n
* @return
*/
public static ListNode removeNthFromEndByTwoPoints(ListNode head, int n) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode first = head;
ListNode second = dummyHead;
for (int i = 0; i < n; i++) {
first = first.next;
}
while (first != null) {
first = first.next;
second = second.next;
}
// 如果first == null,此时 second指向被删除结点的前一结点
second.next = second.next.next;
return dummyHead.next;
}
方法2:遍历链表
遍历链表得到链表长度length,通过 length - n + 1 得到要删除节点的前一节点
/**
* 方法2:删除倒数第N个结点
*
* @param head
* @param n
* @return
*/
public static ListNode removeNthFromEndByLength(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
int length = getLength(head);
for (int i = 1; i < length - n + 1; i++) {
cur = cur.next;
}
cur.next = cur.next.next;
return dummy.next;
}
public static int getLength(ListNode head) {
int length = 0;
while (head != null) {
++length;
head = head.next;
}
return length;
}
5.3 删除升序链表的重复元素
5.3.1 删除重复元素,仅保留一个
两两比较,如果值相同就删除
/**
* 删除升序链表的重复元素,仅保留一个
*
* @param head
* @return
*/
public static ListNode deleteDuplicate(ListNode head) {
if (head == null) {
return head;
}
ListNode cur = head;
while (cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
5.3.2 删除所有重复元素
如果cur.next.val == cur.next.next.val,就删除两个节点 cur.next = cur.next.next;
/**
* 重复元素都不要
*
* @param head
* @return
*/
public static ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode dummy = new ListNode(0);
dummy.next = 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;
// 如果直接使用cur.next = cur.next.next.next; 这样没有处理到链表结尾会导致空指针
while (cur.next != null && cur.next.val == x) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return dummy.next;
}
黄金🍄
1. 链表中环的问题
1.1 HashSet 方法
思路:
- 遍历链表,将链表依次添加到 HashSet 中
- 利用 HashSet 的不允许重复特性判断,如果 !(seen.add(head)) 为false,说明 HashSet 中已有同样的节点,链表为环形链表,返回 true
- 如果不是环形链表,最后一个为null,则返回fale
/**
* 方法1:通过HashSet判断
*
* @param head
* @return
*/
public static boolean hasCycleByMap(ListNode head) {
Set<ListNode> seen = new HashSet<ListNode>();
while (head != null) {
if (!seen.add(head)) {
return true;
}
head = head.next;
}
return false;
}
1.2 快、慢指针
思路:
- 在链表存在环的情况下,为什么快慢指针一定会相遇?
-
- 因为存在环,所以链表的最后一个肯定不为 null;快指针走2步,慢指针走1步;
- 如果链表没有环形,最后会退出while循环,返回false;如果有环形,则会执行slow = slow.next;
fast = fast.next.next; 直到slow == fast,返回true。
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
// 因为fast走的更快,所以如果没有环形的话,fast会先出现空指针情况,这里只判断fast就好了
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if(slow == fast) {
return true;
}
}
return false;
}