链表(Java版)

链表(Java版)

在链表操作中,我们通常通过生成一个 dummy 哑节点,来避免空链表的判断。

19.删除链表的倒数第 N 个结点

方法一:计算链表长度

为了方便删除操作,我们可以从哑节点开始遍历 L−n+1 个节点。当遍历到第 L−n+1 个节点时,它的下一个节点就是我们需要删除的节点,这样我们只需要修改一次指针,就能完成删除操作。

方法三:双指针

我们可以使用两个指针 first 和 second 同时对链表进行遍历,并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。

时间复杂度:O(L),其中 L 是链表的长度。
空间复杂度:O(1)。

21.合并两个有序链表

  1. 当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里。
  2. 当 l1 和 l2 有一个是空的,只需要简单地将非空链表接在合并链表的后面。

23.合并K个升序链表

方法一:顺序合并

已经有合并两个有序链表的前提,直接遍历数组,依次合并两个链表得到结果。

方法二:分治合并

将 k 个链表分组并将同一组中的链表合并(递归实现)

进时间复杂度为 O(kn×logk)
空间复杂度,递归会使用到 O(logk) 空间代价的栈空间

链表相交

编写一个程序,找到两个单链表相交的起始节点。

例如,下面的两个链表:

1
2
3
4
5
A:          a1 → a2

c1 → c2 → c3

B: b1 → b2 → b3

在节点 c1 开始相交。

注意:

  • 如果两个链表没有交点,返回 null.
  • 在返回结果后,两个链表仍须保持原有的结构。
  • 可假定整个链表结构中没有循环。
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

方法一:哈希集合

思路和算法

判断两个链表是否相交,可以使用哈希集合存储链表节点。

首先遍历链表 headA,并将链表 headA 中的每个节点加入哈希集合中。然后遍历链表 headB,对于遍历到的每个节点,判断该节点是否在哈希集合中:

  • 如果当前节点不在哈希集合中,则继续遍历下一个节点;

  • 如果当前节点在哈希集合中,则后面的节点都在哈希集合中,即从当前节点开始的所有节点都在两个链表的相交部分,因此在链表 headB 中遍历到的第一个在哈希集合中的节点就是两个链表相交的节点,返回该节点。

如果链表 headB 中的所有节点都不在哈希集合中,则两个链表不相交,返回 null。

复杂度分析

时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。需要遍历两个链表各一次。

空间复杂度:O(m),其中 m 是链表 headA 的长度。需要使用哈希集合存储链表 headA 中的全部节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> visited = new HashSet<ListNode>();
ListNode temp = headA;
while (temp != null) {
visited.add(temp);
temp = temp.next;
}
temp = headB;
while (temp != null) {
if (visited.contains(temp)) {
return temp;
}
temp = temp.next;
}
return null;
}
}

方法二:双指针

思路和算法

使用双指针的方法,可以将空间复杂度降至 O(1)。

只有当链表 headA 和 headB 都不为空时,两个链表才可能相交。因此首先判断链表 headA 和 headB 是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 null。

当链表 headA 和 headB 都不为空时,创建两个指针 pA 和 pB,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:

  • 每步操作需要同时更新指针 pA 和 pB;

  • 如果指针 pA 不为空,则将指针 pA 移到下一个节点;如果指针 pB 不为空,则将指针 pB 移到下一个节点。

  • 如果指针 pA 为空,则将指针 pA 移到链表 headB 的头节点;如果指针 pB 为空,则将指针 pB 移到链表 headA 的头节点。

  • 当指针 pA 和 pB 指向同一个节点或者都为空时,返回它们指向的节点或者 null。

证明

考虑两种情况,第一种情况是两个链表相交,第二种情况是两个链表不相交。

复杂度分析

时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。两个指针同时遍历两个链表,每个指针遍历两个链表各一次。

空间复杂度:O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}

参考连接:

https://leetcode-cn.com/problems/intersection-of-two-linked-lists/solution/xiang-jiao-lian-biao-by-leetcode-solutio-a8jn/

https://zhuanlan.zhihu.com/p/48313122

链表常见问题

链表常见问题

1
2
3
4
5
6
7
8
9
10
/**
* 单链表数据结构
*/
class Node {
int val;
Node next;
public Node(int val) {
this.val = val;
}
}

0x01 链表翻转

题目描述:

这道算法题,说直白点就是:如何让后一个节点指向前一个节点。

1
2
3
4
5
6
7
8
9
10
11
// 1.翻转链表
public static Node reverse(Node head) {
Node res = null;
while (head != null) {
Node h = head.next;
head.next = res;
res = head;
head = h;
}
return res;
}

0x02 两数相加

题目描述:

Leetcode:给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。

你可以假设除了数字 0 之外,这两个数字都不会以零开头。

示例:

1
2
3
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 2.链表相加
public static Node plus(Node l1, Node l2) {
Node dummyHead = new Node(0);
Node l3 = dummyHead;
int carry = 0;
while (l1 != null || l2 != null || carry > 0) {
int val = carry;
val = (l1 != null ? val + l1.val : val);
val = (l2 != null ? val + l2.val : val);
Node t = new Node(val > 9 ? val - 10 : val);
l3.next = t;
carry = val > 9 ? 1 : 0;
l1 = l1 != null ? l1.next : null;
l2 = l2 != null ? l2.next : null;
l3 = t;
}
return dummyHead.next;
}

0x03 链表中倒数第k个节点

题目描述:

输入一个链表,输出该链表中倒数第k个结点。

问题分析:

链表中倒数第k个节点也就是正数第(L-K+1)个节点。

首先两个节点/指针,一个节点 node1 先开始跑,指针 node1 跑到 k-1 个节点后,另一个节点 node2 开始跑,当 node1 跑到最后时,node2 所指的节点就是倒数第k个节点也就是正数第(L-K+1)个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 3.链表中倒数第k个节点
public static Node findKFromEnd(Node head, int k) {
Node node1 = head, node2 = head;
int len = 0, index = k; // 链表长度
while (node1 != null) {
len++;
node1 = node1.next;

// 从 len = k -1 的位置开始,执行 K 次,
if (index > 0) {
index--;
} else {
node2 = node2.next;
}
}
if (len == 0 || len < k) return null;
return node2;
}

0x04 删除链表的倒数第N个节点

题目描述:

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

问题分析:

  • 两次遍历法

首先我们将添加一个 哑结点(dummyHead)作为辅助,该结点位于列表头部。哑结点用来简化某些极端情况,例如列表中只含有一个结点,或需要删除列表的头部。在第一次遍历中,我们找出列表的长度 L。然后设置一个指向哑结点的指针,并移动它遍历列表,直至它到达第 (L - n) 个结点那里。我们把第 (L - n)个结点的 next 指针重新链接至第 (L - n + 2)个结点,完成这个算法。

  • 一次遍历法:

链表中倒数第N个节点也就是正数第(L-N+1)个节点。 定义两个节点 node1、node2;node1 节点先跑,node1节点 跑到第 n+1 个节点的时候,node2 节点开始跑.当node1 节点跑到最后一个节点时,node2 节点所在的位置就是第 (L-n ) 个节点(L代表总链表长度,也就是倒数第 n+1 个节点)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 4.删除链表的倒数第N个节点
public static Node deleteKFromEnd(Node head, int k) {
Node dummyHead = new Node(0);
dummyHead.next = head;
int len = 0, index = k;
Node p1 = head, p2 = dummyHead;
while (p1 != null) {
len++;
p1 = p1.next;
if (index > 0) {
index--;
} else {
p2 = p2.next;
}
}
if (len >= k && k > 0) {
p2.next = p2.next.next;
}
return dummyHead.next;
}

0x05 合并两个有序的链表

题目描述:

输入两个单调递增的链表,输出两个链表合成后的链表,需要合成后的链表单调递增。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 5.合并两个排序的链表
public static Node merge(Node head1, Node head2) {
if (head1 == null) return head2;
if (head2 == null) return head1;
Node t = null;
if (head1.val < head2.val) {
t = head1;
t.next = merge(head1.next, head2);
} else {
t = head2;
t.next = merge(head1, head2.next);
}
return t;
}

KMP算法(Java版)

KMP算法(Java版)

谈到字符串问题,不得不提的就是 KMP 算法,它是用来解决字符串查找的问题,可以在一个字符串(S)中查找一个子串(W)出现的位置。KMP 算法把字符匹配的时间复杂度缩小到 O(m+n) ,而空间复杂度也只有O(m)。因为“暴力搜索”的方法会反复回溯主串,导致效率低下,而KMP算法可以利用已经部分匹配这个有效信息,保持主串上的指针不回溯,通过修改子串的指针,让模式串尽量地移动到有效的位置。

问题:

有一个文本串S,和一个模式串P,现在要查找P在S中的位置

0x01 暴力算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 暴力查找法:不一致的时候直接把i 移动到 i+1 的位置继续比较
private static int search(String s, String p) {
int sLen = s.length();
int pLen = p.length();
int i = 0, j = 0;
while (i < sLen && j < pLen) {
if (s.charAt(i) == p.charAt(j)) {
// 字符相同,则继续匹配下一个字符
i++;
j++;
} else {
// i,j 复位
i = i - j + 1;
j = 0;
}
}
if (j == pLen) return i - j;
return -1;
}
阅读更多

十大排序算法实现(Java版)

十大排序算法实现(Java版)

一. 常用排序算法

1
2
3
4
5
// Java 的进制数表示
int a10 = 99;
int a2 = 0b101;
int a8 = 0143;
int a16 = 0x63;

0x01 冒泡排序

原理
比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

阅读更多
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×