206-反转链表
题意分析:
- 反转单链表所有节点。
解题思路:
- 迭代。指针一次遍历,迭代反转节点指向。
- 递归。针对每个节点递归调用。
注意点:
- 递归。
- 递归体和反转逻辑的先后顺序。
代码:
/**
* 迭代思路。反转指针的指向。
* @param head
* @return
*/
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = pre;
pre = curr;
curr = next;
}
return pre;
}
/**
* 递归思路。
* 1、找到递归体
* 2、确定退出条件
* @param head
* @return
*/
public ListNode reverseList2(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
92-反转链表II
题意分析:
- 反转单链表中的一段节点。
解题思路:
- 插入排序思想。找到要反转链表节点区域的前驱节点,然后通过插入排序反转。
代码:
public ListNode reverseBetween(ListNode head, int left, int right) {
// 增加哑节点
ListNode dummy = new ListNode();
dummy.next = head;
// 找到 left 处对应的节点,然后从这里开始插入排序逻辑反转
ListNode curr = dummy;
ListNode pre = null;
int a = left;
while (curr != null && a > 0) {
pre = curr;
curr = curr.next;
a--;
}
System.out.println(pre.val + ": " + curr.val);
ListNode next;
// 遍历要插入排序的次数,即要执行插入操作的节点个数
for (int i = 0; i < right - left; i++) {
// System.out.println("i=" + i);
if (curr.next != null) {
next = curr.next;
curr.next = next.next;
next.next = pre.next;
pre.next = next;
}
}
return dummy.next;
}
141-环形链表
题意分析:
- 判断单链表中是否存在环。
解题思路:
- 快慢指针。
注意点:
- 特殊情况处理。比如空链表或者链表只有一个节点。
- while条件的判断。
- fast 指针走两次时第一次为 null 的处理。
代码:
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head, fast = head.next;
// 快慢指针扫描
while (fast != null) {
if (slow == fast) return true;
slow = slow.next;
// fast 走两次时容易空指针
if (fast.next == null) return false;
fast = fast.next.next;
}
return false;
}
142-环形链表II
题意分析:
- 判断单链表是否存在环,找到环的入口
解题思路:
- 快慢指针。先找到快慢指针找到相遇点,然后分析相遇点到入口与head节点到入口的距离同时走向。(注意:fast 指针走的距离永远是 slow 指针的2倍)
注意点:
- 找到相遇点时,如果链表没有环,fast 指针为空,此时要先判断。
代码:
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) {
return null;
}
// 快慢指针
ListNode slow = head, fast = head.next;
// 快慢指针扫描,找到相遇点
while (fast != null) {
// 存在环
if (slow == fast) break;
slow = slow.next;
// fast 走两次时容易空指针
if (fast.next == null) return null;
fast = fast.next.next;
}
// System.out.println(slow.val + ": " + fast.val);
// 找到环的入口 slow和fast相遇。fast走的距离始终等于slow的2倍: L(fast) = 2 L(slow)
// [3,2,0,4,5]-(-1),该demo没有环fast为空
if (fast == null) return null;
fast = fast.next;
ListNode p = head;
while (p != fast) {
p = p.next;
fast = fast.next;
}
// 返回 fast 或者 p
return fast;
}
86-分隔链表
题意解析:
- 将链表节点小于某个节点值的节点移动到该节点前面,并且整体节点顺序不变。
解题思路:
- 插入排序思想。由于要保证每个节点的初始相对位置,很容易想到插入排序,而插入排序思想的核心,无非就是要记录前面所有有序节点的最后一个节点,方便插入新数据。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 合并两个链表。可以考虑将小于和大于等于x的节点单独维护一个链表,记录两个链表的头尾节点,然后合并。
- 时间复杂度:O(n)
- 空间复杂度:O(n)
注意点:
- 插入排序思想。
- 扫描时要注意更新 curr 当前节点的前驱节点 pre 更新,会出现 curr 更新到下一个节点,但 pre 节点不变,这种情况要单独处理。
代码:
/**
* 新构建两个链表,一个存储小于 x 的节点,一个存储大于 x 的节点
* @param head
* @param x
* @return
*/
public ListNode partition(ListNode head, int x) {
ListNode small = new ListNode(Integer.MIN_VALUE);
ListNode smallHead = small;
ListNode large = new ListNode(Integer.MIN_VALUE);
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;
small.next = largeHead.next;
return smallHead.next;
}
/**
* 解题思路:插入排序保证有序。
* @param head
* @param x
* @return
*/
public ListNode partition2(ListNode head, int x) {
// 创建哑节点
ListNode dummy = new ListNode(Integer.MIN_VALUE, head);
// small 记录前面小于 x 的最新节点,pre 为扫描指针 curr 的前驱指针
ListNode small = dummy, pre = dummy, curr = head;
while (curr != null) {
if (curr.val < x) {
ListNode newcurr = curr.next;
pre.next = curr.next;
curr.next = small.next;
small.next = curr;
// curr的pre节点更新需要特殊处理,比如 curr 节点原地更新,pre = curr,否则 pre 就不需要更新
if (pre.next == curr) {
pre = curr;
}
small = curr;
curr = newcurr;
} else {
pre = curr;
curr = curr.next;
}
}
return dummy.next;
}
1171-从链表中删去总和值为零的连续节点(*)
题意分析:
- 链表中连续节点(两个或者多个)如果和为零则删除,如果删除一次后还存在和为零的连续节点还需要继续删除。
解题思路:
- 哈希表记录(思路笔记特殊)。
- 时间复杂度:O(n)
- 空间复杂度:O(n)
- 前缀和(待实现)。
注意点:
- 哈希表记录。
- 难点在于哈希表key覆盖时如何重构链表。
代码:
public ListNode removeZeroSumSublists(ListNode head) {
ListNode dummy = new ListNode(0, head);
Map<Integer, ListNode> map = new HashMap<>();
// 第一次扫描,建立 当前节点和与当前节点 哈希表,若哈希表 节点和 相同则覆盖
int sum = 0;
for (ListNode curr = dummy; curr != null; curr = curr.next) {
sum += curr.val;
map.put(sum, curr);
}
// 根据第一次扫描的哈希表,如果哈希表的key更新过,则说明区间数据需要删除
sum = 0;
for (ListNode curr = dummy; curr != null; curr = curr.next) {
sum += curr.val;
curr.next = map.get(sum).next;
}
return dummy.next;
}
160-相交链表
题意分析:
- 两个链表若相交,返回相交点,否则返回 null。
解题思路:
- 哈希集合。遍历 A 遍历并记录链表节点,然后遍历 B 节点,第一次出现对象值一样的节点则是相交节点。
- 时间复杂度:O(m+n)。
- 空间复杂度:O(m)
- 双指针。A 指针走完 A 链表所有节点和 B 链表相交前的节点距离,B 指针走完 B 链表所有节点和 A 链表相交前的节点距离,两个距离是相等的,也是交叉点位置。
- 时间复杂度:O(m+n)。
- 空间复杂度:O(1)。
注意点:
- 哈希集合。
- 本地测试不太好测试,可能在相交点之前 A 和 B 中也存在相同元素,但元素的哈希值是不同的,这一点在相交情况下也是一致的。
代码:
/**
* 哈希集合(List集合)
*/
public ListNode getIntersectionNode2(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
List<ListNode> list = new ArrayList<>();
for (ListNode p = headA; p != null; p = p.next) {
list.add(p);
}
for (ListNode q = headB; q != null; q = q.next) {
if (list.contains(q)) {
return q;
}
}
return null;
}
/**
* 双指针。
* 当存在交叉,两个指针分别走完两个链表的距离一定是相等的。
*/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pa = headA, pb = headB;
while (pa != pb) {
pa = pa != null ? pa.next : headB;
pb = pb != null ? pb.next : headA;
}
return pa;
}
147-对链表进行插入排序
题意分析:
- 数组的插入排序。
解题思路:
- 参考数组插入排序。
注意点:
- 插入排序时要考虑前面节点有序时插入的位置单独处理。
代码:
public ListNode insertionSortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 增加哑节点,方便统一处理
ListNode dummy = new ListNode(Integer.MIN_VALUE);
dummy.next = head;
// 从第二个节点开始处理
ListNode curr = head.next;
ListNode sort_last = head;
while (curr != null) {
int curr_val = curr.val;
// 两种情况,前面节点有序,一种不修改前面排序状态,一种要修改前面排序状态
if (curr_val >= sort_last.val) {
sort_last = sort_last.next;
} else {
// 从第一个节点开始遍历
ListNode p = dummy;
// 这个位置判断 p.next.val 比较巧妙,这样找不大于 curr_val 的节点时不要单独申请空间记录其前驱节点
while (p.next.val <= curr_val) {
p = p.next;
}
sort_last.next = curr.next;
curr.next = p.next;
p.next = curr;
}
// Utils.printList(dummy.next);
curr = sort_last.next;
}
return dummy.next;
}
148-排序链表(归并排序)
题意分析:
解题思路:
注意点:
代码:
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 寻找链表中节点
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// System.out.println("slow = " + slow.val);
ListNode mid = slow;
ListNode right = mid.next;
mid.next = null;
ListNode l1 = sortList(head);
ListNode l2 = sortList(right);
return merge(l1, l2);
}
/**
* 合并两个有序链表
* @param l1
* @param l2
* @return
*/
public ListNode merge(ListNode l1, ListNode l2) {
ListNode head = new ListNode(Integer.MAX_VALUE);
ListNode cur = head;
ListNode p1 = l1, p2 = l2;
while (p1 != null && p2 != null) {
ListNode tmp;
if (p1.val < p2.val) {
tmp = p1;
p1 = p1.next;
} else {
tmp = p2;
p2 = p2.next;
}
cur.next = tmp;
cur = cur.next;
}
if (p1 != null) cur.next = p1;
if (p2 != null) cur.next = p2;
return head.next;
}
19-删除链表的倒数第 N 个节点
题意分析:
- 给定链表,删除倒数的某一个节点。
解题思路:
- 两次扫描。第一次计算链表长度,第二次找到要删除的节点。
- 时间复杂度:O(2n)
- 空间复杂度:O(1)
- 双指针。快慢指针同时扫描,快指针先走 n 个节点,然后快慢指针同时走,但快指针走到尾节点时,满指针就是倒数第 n 个节点(快慢指针始终相差 n 个节点)。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 辅助栈。先将链表元素都入栈,然后依次出栈元素到倒数第 n 个节点。
- 时间复杂度:O(n)
- 空间复杂度:O(n)
注意点:
代码:
/**
* 思路一:两次扫描链表,第一次获取链表长度,第二次找到要删除的节点
* @param head
* @param n
* @return
*/
public ListNode removeNthFromEnd1(ListNode head, int n) {
ListNode nHead = new ListNode(-1, head);
ListNode p = nHead;
ListNode idx = nHead, idxPre = nHead;
int len = 0;
// 统计链表长度
while (p != null) {
p = p.next;
len++;
}
// 要遍历的元素位置
int k = len - n + 1;
int i = 1;
while (i++ < k) {
idxPre = idx;
idx = idx.next;
}
if (idxPre == nHead) {
idxPre.next = null;
} else {
idxPre.next = idx.next;
}
return nHead.next;
}
/**
* 思路二:利用辅助栈,出栈时找对要删除的元素
* @param head
* @param n
* @return
*/
public ListNode removeNthFromEnd2(ListNode head, int n) {
ListNode nHead = new ListNode(-1, head);
Deque<ListNode> stack = new LinkedList<>();
ListNode p = nHead;
while (p != null) {
stack.push(p);
p = p.next;
}
ListNode idx = nHead;
// 退到要删除的元素为止
for (int i = 0; i < n; i++) {
idx = stack.pop();
}
ListNode idxPre = nHead;
if (stack.isEmpty()) {
idx.next = null;
} else {
idxPre = stack.pop();
idxPre.next = idx.next;
}
return nHead.next;
}
/**
* 思路三:利用快慢指针,两个指针始终相差n个元素,当快指针扫描到链表最后一个元素,此时慢指针所在的元素就是要删除的元素
* @param head
* @param n
* @return
*/
public ListNode removeNthFromEnd3(ListNode head, int n) {
ListNode nHead = new ListNode(-1, head);
// 快慢指针
ListNode fast = nHead, slow = nHead;
// 辅助指针
ListNode p = nHead, slowPre = nHead;
// 快指针先走了n个位置
for(int i = 1; i <= n; i++){
fast = p;
p = p.next;
}
while (fast.next != null) {
fast = fast.next;
slowPre = slow;
slow = slow.next;
}
// 特殊情况,如果链表只有一个元素,slowPre 其实和 slow 指针是对等的,无法按照正常逻辑去处理
if (slowPre == slow) {
slowPre.next = null;
} else {
slowPre.next = slow.next;
}
return nHead.next;
}
61-旋转链表
题意分析:
解题思路:
注意点:
代码:
public ListNode rotateRight(ListNode head, int k) {
if (k == 0 || head == null || head.next == null) {
return head;
}
int len = 1;
ListNode p = head;
// 遍历到链表尾节点
while (p.next != null) {
len++;
p = p.next;
}
// 链表尾部和头部形成闭环
p.next = head;
// 记录从尾部指针移动多少次走到新尾部节点
int mvLen = len - k % len;
while (mvLen-- > 0) {
p = p.next;
}
ListNode newHead = p.next;
p.next = null;
return newHead;
}
去重问题
83-删除排序链表中的重复元素
题意分析:
解题思路:
注意点:
代码:
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode dummy = new ListNode(-101, head);
ListNode cur = dummy;
while (cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return dummy.next;
}
82-删除排序链表中的重复元素II
题意分析:
解题思路:
注意点:
代码:
public ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return head;
}
ListNode dummy = new ListNode(-101, head);
ListNode cur = dummy;
while (cur.next != null && cur.next.next != null) {
if (cur.next.val == cur.next.next.val) {
// 开始遍历重复元素个数,并删除
int tmp = cur.next.val;
while (cur.next != null && cur.next.val == tmp) {
cur.next = cur.next.next;
}
} else {
cur = cur.next;
}
}
return dummy.next;
}
未分类
24-两两交换链表中的节点
题意分析:
解题思路:
注意点:
代码:
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
ListNode temp = dummyHead;
while (temp.next != null && temp.next.next != null) {
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1;
}
return dummyHead.next;
}
21-合并两个有序链表
题意分析:
解题思路:
注意点:
代码:
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(Integer.MAX_VALUE);
ListNode cur = head;
ListNode p1 = l1, p2 = l2;
while (p1 != null && p2 != null) {
ListNode tmp;
if (p1.val < p2.val) {
tmp = p1;
p1 = p1.next;
} else {
tmp = p2;
p2 = p2.next;
}
cur.next = tmp;
cur = cur.next;
}
if (p1 != null) cur.next = p1;
if (p2 != null) cur.next = p2;
return head.next;
}
23-合并K个升序链表
题意分析:
- 合并 K 个升序链表。
解题思路:
- 优先级队列(最小堆)。队列先添加所有链表的 head 节点(每次添加都是最小节点),然后取队列元素并添加其 next 元素到队列中。
- 时间复杂度:优先级队列中最多有 k 个元素,单个节点插入和删除的时间复杂度为 O(logk),总共有 kn 个节点,每个节点都要被插入和删除一次,故总的时间复杂度为 O(kn * logk)。
- 空间复杂度:优先级队列的大小 O(k)。
注意点:
- 优先级队列添加对象时需要实现 Compartor 对象。
- TreeSet 可以实现按顺序添加元素和取最小元素,但不能添加重复元素。
扩展:
代码:
/**
* 合并 k 个升序链表
*
* 思路:
* 1、添加所有链表的 head 节点
* 2、循环从中拿到最小节点,然后添加其 next 元素
*/
public ListNode mergeKLists(ListNode[] lists) {
// 构建优先级队列(最小堆),队列元素添加排序规则
Queue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() {
// 升序排序器,默认就是最小堆
@Override
public int compare(ListNode o1, ListNode o2) {
return o1.val - o2.val;
}
});
// 添加所有链表的 head 节点
for (ListNode list : lists) {
if (list != null)
queue.offer(list);
}
// 增加哨兵节点
ListNode newHead = new ListNode(-1);
ListNode currTail = newHead;
// 循环出队列,并添加新元素到队列
while (!queue.isEmpty()) {
ListNode node = queue.poll();
if (node.next != null) {
queue.offer(node.next);
}
currTail.next = node;
currTail = node;
node.next = null;
}
return newHead.next != null ? newHead.next : null;
}
1669-合并两个链表
题意分析:
解题思路:
注意点:
代码:
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
// 获取链表 1 的前半部分结束节点 和 后半部分的开始节点
ListNode head1 = list1, tail1 = list1;
ListNode curr1 = list1;
while (curr1 != null && b > 0) {
if (a > 0) {
head1 = curr1;
a--;
}
curr1 = curr1.next;
b--;
}
tail1 = curr1.next;
System.out.println("head1 = " + head1.val + "; tail1 = " + tail1.val);
// 获取链表 2 的头节点和尾节点
ListNode head2 = list2;
while (list2.next != null) {
list2 = list2.next;
}
ListNode tail2 = list2;
System.out.println("head2 = " + head2.val + "; tail2 = " + tail2.val);
// 连接链表
head1.next = head2;
tail2.next = tail1;
return list1;
}
模版
题意分析:
解题思路:
注意点:
扩展:
代码: