常考手撕代码

一、排序算法

下图是十大经典的排序算法

十大经典的排序算法
十大经典的排序算法

1.1 冒泡排序

1.1.1 算法思想

重复遍历要排序的序列,依次比较两个元素,如果顺序错误,就交换位置。

冒泡排序
冒泡排序

1.1.2 算法步骤

  1. 比较相邻元素。如果第一个比第二个大,就换位置;
  2. 对每一对相邻元素做同样的工作,从开始第一队到结尾的最后一对,这样每次都能找出最大的一个;
  3. 针对所有元素重复以上步骤;
  4. 重复1-3步,直到排序完成。

1.1.3 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 冒泡排序,从小到大
* nums = [1,8,6,7,9,2];
* @param nums
*/
public static int[] bubbleSort(int[] nums){
for(int i = nums.length - 1; i > 0; i--){
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]){
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
return nums;
}

1.1.4 算法分析

  • 稳定性:稳定
  • 时间复杂度:最佳:O(n) ,最差:O(n^2), 平均:O(n^2)
  • 空间复杂度:O(1)
  • 排序方式:占用常数内存,不占用额外内存

1.2 选择排序

1.2.1 算法思想

在未排序的序列中找到最小的元素,然后存放在最开始的位置,然后继续从未排序的元素中找最小的,以此类推。

特点就是不论什么顺序的数据,时间复杂度都是O(n^2)。

Selection Sort
Selection Sort

1.2.2 算法步骤

  1. 首先在未排序的序列中找最小的元素,放到序列最开始的位置;
  2. 然后在剩下的未排序序列中找最小的元素,放到已排序的序列末尾;
  3. 重复第二步,直到排序结束。

1.2.3 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 选择排序,从小到大
* @param nums
* @return
*/
public static int[] selectionSort(int[] nums){
for (int i = 0; i < nums.length; i++) {
int min = Integer.MAX_VALUE, index = 0;
for (int j = i; j < nums.length; j++){
if (nums[j] < min){
min = nums[j];
index = j;
}
}
int temp = nums[index];
nums[index] = nums[i];
nums[i] = temp;
}
return nums;
}

1.2.4 算法分析

  • 稳定性:不稳定
  • 时间复杂度:最佳:O(n^2) ,最差:O(n^2), 平均:O(n^2)
  • 空间复杂度:O(1)
  • 排序方式:不需要额外空间

1.3 快速排序

1.3.1 算法思想

用到了分治思想,将问题划分为子问题,然后排序子串,最后合并。

通过一趟排序将待排序列分为独立的两部分,其中一部分记录的元素均比另一部分的元素小,则可分别对这两部分子序列继续进行排序,以达到整体有序。

RandomQuickSort
RandomQuickSort

1.3.2 算法步骤

  1. 从序列中随机挑选一个元素,作为基准(pivot);
  2. 重新排序,将所有比基准值小的元素摆放在基准前面,比基准值大的元素摆放在基准的后面。基准就位于中间位置,这就是分区(partition)。
  3. 递归地把小于基准元素的子序列和大于基准元素的子序列进行快排。

1.3.3 代码实现

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
30
31
32
33
34
35
36
37
38
39
/**
* 快排,递归
* @param nums
* @param low
* @param high
*/
private static void quickSort(int[] nums, int low, int high) {
if (low < high){
int position = partition(nums, low, high);
quickSort(nums, low, position - 1);
quickSort(nums, position + 1, high);
}
}

/**
* 分区,返回每次分区的基值
* @param nums
* @param low
* @param high
* @return
*/
private static int partition(int[] nums, int low, int high) {
int pivot = nums[high];
int index = low;
for (int i = low; i < high; i++) {
if (nums[i] <= pivot){
swap(nums, i, index);
index++;
}
}
swap(nums, index, high);
return index;
}

public static void swap(int[] num, int i, int j){
int temp = num[i];
num[i] = num[j];
num[j] = temp;
}

1.3.4 算法分析

  • 稳定性:不稳定
  • 时间复杂度:最佳:O(nlogn), 最差:O(nlogn),平均:O(nlogn)
  • 空间复杂度:O(nlogn)

1.3.5 优化快排

在快排的基础上加了随机和去重。

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
30
31
32
33
34
35
36
37
38
39
private static void quickSort(int[] nums, int low, int high){
if (low < high){
int position = partition(nums, low, high);
int leftPosition = position - 1;
int rightPosition = position + 1;
//去重
while (leftPosition > low && nums[leftPosition] == nums[position]){
leftPosition--;
}
//去重
while (rightPosition < high && nums[rightPosition] == nums[position]){
rightPosition++;
}
quickSort(nums, low, leftPosition);
quickSort(nums, rightPosition, high);
}
}

private static int partition(int[] nums, int low, int high) {
//随机
int randomIndex = low + (int)Math.random() *(high - low + 1);
swap(nums, high, randomIndex);
int pivot = nums[high];
int index = low;
for (int i = low; i < high; i++){
if (nums[i] <= pivot){
swap(nums, i, index);
index++;
}
}
swap(nums, index, high);
return index;
}

private static void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}

1.4 堆排序

1.4.1 算法思想

利用堆的数据结构特性,即子节点的值总是小于(或大于)其父节点。

HeapSort
HeapSort

1.4.2 算法步骤

  1. 将初始待排序列构建成大顶堆,次堆为初始的无序区。
  2. 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区 (R1, R2, ……, Rn-1) 和新的有序区 (Rn), 且满足 R[1, 2, ……, n-1]<=R[n]
  3. 由于交换后的新堆R[1]可能违反堆的性质,因此需要对当前无序区重新调整,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区和有序区。不断重复,直到排序过程完成。

1.4.3 代码实现

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 全局变量,用来记录当前堆的长度
public static int heapLen;

/**
* 堆排序
* @param nums
* @return
*/
public static int[] heapSort(int[] nums) {
heapLen = nums.length;
buildMaxHeap(nums);
for (int i = nums.length - 1; i > 0; i--) {
swap(nums, 0, i);
heapLen--;
heapify(nums, 0);
}
return nums;
}

/**
* 建立大顶堆
* @param nums
*/
private static void buildMaxHeap(int[] nums) {
for (int i = nums.length / 2 - 1; i >= 0; i--) {
heapify(nums, i);
}
}

/**
* 调整大顶堆
* @param nums
* @param i
*/
private static void heapify(int[] nums, int i) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < heapLen && nums[left] > nums[largest]){
largest = left;
}
if (right < heapLen && nums[right] > nums[largest]){
largest = right;
}
if (largest != i){
swap(nums, i, largest);
heapify(nums, largest);
}
}

private static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}

1.4.4 算法分析

  • 稳定性:不稳定
  • 时间复杂度:最佳:O(nlogn), 最差:O(nlogn), 平均:O(nlogn)
  • 空间复杂度:O(1)

1.5 归并排序

1.5.1 算法思想

也是采用分治算法的典型,将已有序的子序列合并,得到完全有序的序列,也称为2路归并。代价是需要额外的内存空间。

MergeSort
MergeSort

1.5.2 算法步骤

  1. 如果输入只有一个元素,则直接返回,否则将长度为n的输入序列分为两个长度为n/2的子序列;
  2. 分别对这两个子序列进行归并排序,使子序列变为有序状态;
  3. 设定两个指针,分别指向两个已经排序子序列的起始位置;
  4. 比较两个指针所指向的元素,选择相对较小的放入到合并空间,并移动指针;
  5. 重复第3步和第4步,直到全部有序

1.5.3 代码实现

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    /**
* 归并排序
* @param nums
* @return
*/
private static void mergeSort(int[] nums, int low, int high) {
if (low < high){
int mid = (low + high) / 2;
//归并左边
mergeSort(nums, low, mid);
//归并右边
mergeSort(nums, mid + 1, high);
//合并两边的数组
merge(nums, low, mid, high);
}
}

/**
* 合并两个数组
* @param nums
* @param low
* @param mid
* @param high
*/
private static void merge(int[] nums, int low, int mid, int high) {
int[] arr = new int[high - low + 1];
int index = 0;
int left = low, right = mid + 1;
//看谁小,谁就先放前面
while(left <= mid && right <= high){
if (nums[left] < nums[right]){
arr[index++] = nums[left++];
}else{
arr[index++] = nums[right++];
}
}
//如果左边还没合并完
while(left <= mid){
arr[index++] = nums[left++];
}
//如果右边还没合并完
while (right <= high){
arr[index++] = nums[right++];
}
//将合并好的结果放回原来的数组
for (int i = 0; i < arr.length; i++) {
nums[i + low] = arr[i];
}
}
}

1.5.4 算法分析

  • 稳定性:稳定
  • 时间复杂度:最佳:O(nlogn), 最差:O(nlogn), 平均:O(nlogn)
  • 空间复杂度:O(n)

二、红黑树

2.1 红黑树性质

红黑树是一种近似平衡的二叉查找树,但并非绝对平衡,但是可以保证任何一个节点的左右子树的高度差不会超过二者中较低的那个的一倍

特点:

  • 每个节点要么是黑色,要么是红色。
  • 根节点是黑色。
  • 每个叶子节点(NULL)是黑色。
  • 每个红色结点的两个子结点一定都是黑色。
  • 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
img
img

红黑树为什么能自平衡?

红黑树总是通过旋转和变色达到自平衡

左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。

右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。

变色:结点的颜色由红变黑或由黑变红。

img
img
img
img

左旋只影响旋转结点和其右子树的结构,把右子树的结点往左子树挪了。
右旋只影响旋转结点和其左子树的结构,把左子树的结点往右子树挪了。

2.2 红黑树查找

  1. 从根结点开始查找,把根结点设置为当前结点;

  2. 若当前结点为空,返回null;

  3. 若当前结点不为空,用当前结点的key跟查找key作比较;

  4. 若当前结点key等于查找key,那么该key就是查找目标,返回当前结点;

  5. 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤2;

  6. 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤2;

img

2.3 红黑树插入

首先找插入的位置,然后再自平衡。

  1. 从根结点开始查找;

  2. 若根结点为空,那么插入结点作为根结点,结束。

  3. 若根结点不为空,那么把根结点作为当前结点;

  4. 若当前结点为null,返回当前结点的父结点,结束。

  5. 若当前结点key等于查找key,那么该key所在结点就是插入结点,更新结点的值,结束。

  6. 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤4;

  7. 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤4;

情况1:红黑树为空树

直接把插入的节点作为根节点,并把该节点设为黑色。

情况2:插入节点的key已经存在

  • 把I设为当前节点的颜色
  • 更新当前结点的值为插入节点的值

情况3:插入节点的父节点为黑节点

这种情况不会影响红黑树平衡,直接插入即可。

情况4:插入节点的父节点为红节点

情况4.1:叔叔节点存在且为红节点

因为其祖父节点肯定存在,且肯定为黑色,所以此时颜色的情况为:黑红红

那么最简单的处理方法就是改为:红黑红

img
img

情况4.2:叔叔节点不存在或为黑色节点,且父节点是祖父节点的左子节点

  1. 插入节点是其父节点的左子节点
  • 将P设为黑色
  • 将PP设为红色
  • 对PP进行右旋
img
img
  1. 插入节点是其父节点的右子节点
  • 对P进行左旋
  • 把P设置为插入结点,得到情景4.2.1
  • 进行情景4.2.1的处理
img
img

情况4.3:叔叔节点不存在或为黑色节点,且父节点是祖父节点的右子节点

  1. 插入节点是父节点的右子节点
  • 将P设为黑色
  • 将PP设为红色
  • 对PP进行左旋
img
img
  1. 插入节点是父节点的左子节点
  • 对P进行右旋
  • 把P设置为插入结点,得到情景4.3.1
  • 进行情景4.3.1的处理
img
img

2.4 红黑树删除

情况1:替换节点是红色

颜色变为删除节点的颜色

情况2:替换节点是黑色

情况2.1:替换节点是其父节点的左子节点

  1. 替换节点的兄弟节点是红节点
  • 将S设为黑色
  • 将P设为红色
  • 对P进行左旋,得到情景2.1.2.3
  • 进行情景2.1.2.3的处理
img
img
  1. 替换节点的兄弟节点是黑节点

    1. 替换节点的兄弟节点的右子节点是红节点,左子节点任意颜色

    直接向右子树“借”个红结点来补充黑结点

    • 将S的颜色设为P的颜色
    • 将P设为黑色
    • 将SR设为黑色
    • 对P进行左旋
    img
    img
  2. 替换节点的兄弟节点的右子节点为黑色,左子节点为红色

向兄弟子树借个红结点过来

  • 将S设为红色
  • 将SL设为黑色
  • 对S进行右旋,得到情景2.1.2.1
  • 进行情景2.1.2.1的处理
img
img
  1. 替换节点的兄弟节点的子节点都为黑色

把兄弟结点设为红色,再把父结点当作替代结点,自底向上处理,去找父结点的兄弟结点去“借”

  • 将S设为红色
  • 把P作为新的替换结点
  • 重新进行删除结点情景处理
img
img

情况2.2 替换节点是其父节点的右子节点

  1. 替换节点的兄弟节点是红节点
  • 将S设为黑色
  • 将P设为红色
  • 对P进行右旋,得到情景2.2.2.3
  • 进行情景2.2.2.3的处理
img
img
  1. 替换节点的兄弟节点是黑节点

    1. 替换节点的兄弟节点的左子节点是红节点,右子节点任意颜色
    • 将S的颜色设为P的颜色
    • 将P设为黑色
    • 将SL设为黑色
    • 对P进行右旋
    img
    img
  2. 替换节点的兄弟节点的左子节点是黑节点,右子节点是红节点

  • 将S设为红色
  • 将SR设为黑色
  • 对S进行左旋,得到情景2.2.2.1
  • 进行情景2.2.2.1的处理
img
img
  1. 替换节点的兄弟节点的子节点都是黑色
  • 将S设为红色
  • 把P作为新的替换结点
  • 重新进行删除结点情景处理
img
img

综上,红黑树删除后自平衡的处理可以总结为:

  1. 自己能搞定的自消化(情景1)
  2. 自己不能搞定的叫兄弟帮忙(除了情景1、情景2.1.2.3和情景2.2.2.3)
  3. 兄弟都帮忙不了的,通过父母,找远方亲戚(情景2.1.2.3和情景2.2.2.3)

三、常考手撕

直接看这个网站CodeTop企业题库

3.1 无重复字符的最长子串

3. 无重复字符的最长子串 - 力扣(Leetcode)

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

3.1.1 方法一:HashSet+滑动窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.length() <= 1){
return s.length();
}
int ans = 0, index = 0;
HashSet<Character> set = new HashSet<>();
for(int i = 0; i < s.length(); i++){
char c = s.charAt(i);
if(set.contains(c)){
ans = Math.max(ans, set.size());
while(s.charAt(index) != c){
set.remove(s.charAt(index));
index++;
}
set.remove(s.charAt(index));
index++;
}
set.add(c);
}
return Math.max(ans, set.size());
}
}

3.1.2 方法二:双指针+滑动窗口+字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public int lengthOfLongestSubstring(String s) {
int ans = 0, index = 0;
int[] dict = new int[128];
for(int i = 0; i < 128; i++){
dict[i] = -1;
}
for(int i = 0; i < s.length(); i++){
index = Math.max(index, dict[s.charAt(i)] + 1);
ans = Math.max(ans, i - index + 1);
dict[s.charAt(i)] = i;
}
return ans;
}
}

3.2 反转链表

206. 反转链表 - 力扣(Leetcode)

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

img
img
1
2
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

3.2.1 方法一:迭代法

img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null){
return null;
}
ListNode p = head, q = null;
while(p != null){
ListNode temp = p.next;
p.next = q;
q = p;
p = temp;
}
return q;
}
}

3.2.2 方法二:递归法

img
1
2
3
4
5
6
7
8
9
10
11
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}

3.2.3 方法三:头插法(优化迭代)

该方法比常规的迭代法内存消耗要低很多,属于优化版的迭代法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode newHead = new ListNode(0);
ListNode p = head, q = null;
while(p != null){
q = p.next;
p.next = newHead.next;
newHead.next = p;
p = q;
}
return newHead.next;
}
}

3.3 LRU缓存

146. LRU 缓存 - 力扣(Leetcode)

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4

3.3.1 方法一:LinkedHashMap(不推荐)

如果实在不记得怎么写,可以取巧用LinkedHashMap来实现,因为LinkedHashMap的特性就是双向链表+HashMap,但是面试官想考的并不是LinkedHashMap。

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
30
31
32
33
34
class LRUCache {

private int cap;
private Map<Integer, Integer> map = new LinkedHashMap<>();

public LRUCache(int capacity) {
this.cap = capacity;
}

public int get(int key) {
if(map.keySet().contains(key)){
int value = map.get(key);
//保证每次查询,都在最后
map.remove(key, value);
map.put(key, value);
return value;
}
return -1;
}

public void put(int key, int value) {
if(map.keySet().contains(key)){
map.remove(key, map.get(key));
}else if(map.size() == cap){
// 使用Iterator遍历并删除第一个元素
Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
if (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
}
map.put(key, value);
}
}

3.3.2 方法二:双向链表+HashMap(推荐)

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class LRUCache {

// 手写双向链表
class DLinkedNode{
public int key;
public int value;
public DLinkedNode pre;
public DLinkedNode next;

public DLinkedNode(){}
public DLinkedNode(int key, int value){
this.key = key;
this.value = value;
}
}

//删除节点
public void removeNode(DLinkedNode node){
node.next.pre = node.pre;
node.pre.next = node.next;
}

//将节点添加到最前面
public void addToHead(DLinkedNode node){
head.next.pre = node;
node.next = head.next;
head.next = node;
node.pre = head;
}

//将指定节点移动到最前面
public void moveToHead(DLinkedNode node){
removeNode(node);
addToHead(node);
}

//删除最后面节点,并返回该节点
public DLinkedNode removeLastNode(){
DLinkedNode resNode = tail.pre;
removeNode(resNode);
return resNode;
}

public HashMap<Integer, DLinkedNode> map = new HashMap<>();
public int mapCap;
public int size; //记录链表中节点个数
public DLinkedNode head, tail;

public LRUCache(int capacity) {
this.size = 0;
this.mapCap = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.pre = head;
}

public int get(int key) {
DLinkedNode node = map.get(key);
//如果节点存在,那么就将节点移到最前面,并返回value
if(node != null){
moveToHead(node);
return node.value;
}else{
return -1;
}
}

public void put(int key, int value) {
DLinkedNode node = map.get(key);
//如果节点不存在
if(node == null){
//新建一个节点,添加到最前面,并使得size++
node = new DLinkedNode(key, value);
map.put(key, node);
addToHead(node);
size++;
//如果链表中节点个数大于定义的最大容量,就移除链表中最后一个元素
if(size > mapCap){
DLinkedNode lastNode = removeLastNode();
//并在map中也删除这个节点
map.remove(lastNode.key);
size--;
}
}else{
//如果节点存在,那么把节点移动到最前面,然后更新节点的value
moveToHead(node);
node.value = value;
}
}
}

3.4 数组中第k大的元素

215. 数组中的第K个最大元素 - 力扣(Leetcode)

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

1
2
输入: [3,2,1,5,6,4], k = 2
输出: 5

3.4.1 方法一:随机快排

快排的时间复杂度不符合要求的O(n),必须得用随机快排。

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
30
31
32
33
34
35
class Solution {
public int findKthLargest(int[] nums, int k) {
quickSort(nums, 0, nums.length - 1);
return nums[nums.length - k];
}

public void quickSort(int[] nums, int low, int high){
if(low < high){
int position = partition(nums, low, high);
quickSort(nums, low, position - 1);
quickSort(nums, position + 1, high);
}
}

public int partition(int[] nums, int low, int high){
int randomIndex = low + (int)Math.random()*(high - low + 1);
swap(nums, randomIndex, high);
int povit = nums[high];
int index = low;
for(int i = low; i < high; i++){
if(nums[i] <= povit){
swap(nums, i, index);
index++;
}
}
swap(nums, high, index);
return index;
}

public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}

3.4.2 方法二:堆排序

堆排序其实不符合题中要求的O(n),但是面试官经常会问一句如果用堆排序该怎么做。可以维护一个只有k个元素的小根堆,堆顶元素就是答案。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution {
public int findKthLargest(int[] nums, int k) {
//维护一个只有k个元素的小根堆
int[] arr = new int[k];
for (int i = 0; i < k; i++) {
arr[i] = nums[i];
}
buildHeap(arr, k - 1);
for (int i = k; i < nums.length; i++) {
if (nums[i] > arr[0]) {
arr[0] = nums[i];
heapify(arr, k - 1, 0);
}
}
return arr[0];
}
private void buildHeap(int[] nums, int n) {
for (int i = n / 2; i >= 0; i--) {
heapify(nums, n, i);
}
}
private void heapify(int[] nums, int n, int i) {
while (true) {
int minPos = i;
int left = 2 * i + 1;
int right = left + 1;
if (left <= n && nums[minPos] > nums[left]){
minPos = left;
}
if (right <= n && nums[minPos] > nums[right]){
minPos = right;
}
if (minPos == i){
break;
}
swap(nums, i, minPos);
i = minPos;
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}

3.4.3 方法三:优先队列

其实本质上还是小根堆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int findKthLargest(int[] nums, int k) {
//创建一个优先队列
PriorityQueue<Integer> minTree = new PriorityQueue<>();
for(int i = 0; i < nums.length; i++){
//将每一个元素都加入到优先队列中
minTree.add(nums[i]);
//维护优先队列中元素为k个,大于k的就弹出
if(minTree.size() > k){
minTree.poll();
}
}
//最后弹出的那个一定是第k大的元素
return minTree.peek();
}
}

3.5 K个一组翻转链表

25. K 个一组翻转链表 - 力扣(Leetcode)

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

img
img
1
2
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

3.5.1 方法一:尾插法

先统计链表长度,然后第一重循环看一共要翻转几次,第二重循环控制每次翻转要翻转k-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
25
26
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
int listLength = 0;
ListNode newHead = new ListNode(0), p = newHead, q = head, temp;
newHead.next = head;
//统计链表长度
while(head != null){
listLength++;
head = head.next;
}
head = newHead.next;
//要翻转listLength/k组
for(int i = 0; i < listLength / k; i++){
//每组内要翻转k - 1次
for(int j = 0; j < k - 1; j++){
temp = q.next;
q.next = temp.next;
temp.next = p.next;
p.next = temp;
}
p = q;
q = p.next;
}
return newHead.next;
}
}

3.5.2 方法二:栈

因为栈的特性就是先进后出,每次压入k个节点,那么弹出的顺序就是翻转后的顺序。

只不过需要注意,如果最后不足k个节点,就不翻转链表。

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
30
31
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
Deque<ListNode> stack = new ArrayDeque<>();
ListNode newHead = new ListNode(0);
ListNode p = newHead;
while(true){
//用count记录stack中节点个数
int count = 0;
ListNode q = head;
//使得k个一组节点陆续入栈
while(q != null && count < k){
stack.add(q);
q = q.next;
count++;
}
//当节点个数不足k个时,说明stack中的节点就不需要翻转了,直接连接上就可以跳出循环
if(count != k){
p.next = head;
break;
}
//将节点出栈,也即翻转k个节点
while(!stack.isEmpty()){
p.next = stack.pollLast();
p = p.next;
}
p.next = q;
head = q;
}
return newHead.next;
}
}

3.5.3 方法三:递归法

  1. 找到待翻转的k个节点(如果结点数少于k的话就不用翻转);
  2. 对k个节点进行翻转操作,并返回反转后的头结点(反转的区间为左开右闭,所以本轮操作的尾结点就是下一轮操作的头结点);
  3. 对下一轮k个节点同样进行翻转操作;
  4. 将上一轮翻转后的尾结点指向下一轮翻转后的头结点。
img
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
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode tail = head;
for (int i = 0; i < k; i++) {
//剩余数量小于k的话,则不需要反转。
if (tail == null) {
return head;
}
tail = tail.next;
}
// 反转前 k 个元素
ListNode newHead = reverse(head, tail);
//下一轮的开始的地方就是tail
head.next = reverseKGroup(tail, k);
return newHead;
}

private ListNode reverse(ListNode head, ListNode tail) {
ListNode pre = null;
ListNode next = null;
while (head != tail) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}

3.6 三数之和

15. 三数之和 - 力扣(Leetcode)

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

1
2
3
4
5
6
7
8
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

3.6.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ansList = new ArrayList<>();
Arrays.sort(nums);
//判断排序后的开始和结尾元素是否符合要求,不符合的话直接退出即可
if(nums[0] > 0 || nums[nums.length - 1] < 0){
return ansList;
}
for(int i = 0; i < nums.length - 2; i++){
//如果第一个都不符合要求,那后面肯定都不行
if(nums[i] > 0){
return ansList;
}
//去重,如果前面一个元素和当前元素相同,直接跳过,不然肯定添加的是相通的
if(i > 0 && nums[i] == nums[i - 1]){
continue;
}
int left = i + 1, right = nums.length - 1;
while(left < right){
int target = nums[i] + nums[left] + nums[right];
if(target < 0){
left++;
}else if(target > 0){
right--;
}else{
List<Integer> temp = new ArrayList<>();
temp.add(nums[i]);
temp.add(nums[left]);
temp.add(nums[right]);
ansList.add(temp);
//去重,同样是找相邻是否有相同的元素,有的话就跳过
while(left < right && nums[left] == nums[left + 1]){
left++;
}
//去重
while(left < right && nums[right] == nums[right - 1]){
right--;
}
left++;
right--;
}
}
}
return ansList;
}
}

3.6.2 方法二:双指针+HashSet去重

思路是一样的,只不过使用HashSet去重,时间复杂度和空间复杂度要差很多,就不写代码了。

3.7 手撕快排

912. 排序数组 - 力扣(Leetcode)

给你一个整数数组 nums,请你将该数组升序排列。

优化过的快排:随机选取基准元素+去重

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}

public void quickSort(int[] nums, int low, int high){
if(low < high){
int position = partition(nums, low, high);
int leftPosition = position - 1;
int rightPosition = position + 1;
//去掉重复的元素
while(leftPosition > low && nums[position] == nums[leftPosition]){
leftPosition--;
}
while(rightPosition < high && nums[position] == nums[rightPosition]){
rightPosition++;
}
quickSort(nums, low, leftPosition);
quickSort(nums, rightPosition, high);
}
}

public int partition(int[] nums, int low, int high){
//随机选取基准元素
int randomIndex = low + (int)Math.random() * (high - low + 1);
swap(nums, randomIndex, high);
int pivot = nums[high];
int index = low;
for(int i = low; i < high; i++){
if(nums[i] <= pivot){
swap(nums, index, i);
index++;
}
}
swap(nums, high, index);
return index;
}

public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}

3.8 最大子数组和

53. 最大子数组和 - 力扣(Leetcode)

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

1
2
3
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

3.8.1 方法一:DP法

每次操作有两种可能:

  1. 前面所求最大元素max加上当前元素nums[i];
  2. 不带前面玩,只用当前元素nums[i]。

然后每次完毕之后,用ans保存最大的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int maxSubArray(int[] nums) {
if(nums.length == 1){
return nums[0];
}
int max = 0;
int ans = Integer.MIN_VALUE;
for(int i = 0; i < nums.length; i++){
max = Math.max(nums[i] + max, nums[i]);
ans = Math.max(max, ans);
}
return ans;
}
}

3.8.2 方法二:贪心法

相当于把所有结果加起来,如果sum小于0,那说明前面的都不作数,重新将sum置为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution{
public int maxSubAray(int[] nums){
//类似寻找最大最小值的题目,初始值一定要定义成理论上的最小最大值
int result = Integer.MIN_VALUE;
int numsSize = nums.length;
int sum = 0;
for (int i = 0; i < numsSize; i++){
sum += nums[i];
result = Math.max(result, sum);
//如果sum < 0,重新开始找子序串
if (sum < 0){
sum = 0;
}
}

return result;
}
}

3.8.3 方法三:分治法

img
img
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public int maxSubAray(int[] nums) {
if (nums == null || nums.length <= 0)// 输入校验
return 0;
int len = nums.length;// 获取输入长度
return getInfo(nums, 0, len - 1).mSum;
}

class wtevTree { //线段树
int lSum;// 以左区间为端点的最大子段和
int rSum;// 以右区间为端点的最大子段和
int iSum;// 区间所有数的和
int mSum;// 该区间的最大子段和
wtevTree(int l, int r, int i, int m) { // 构造函数
lSum = l;
rSum = r;
iSum = i;
mSum = m;
}
}

// 通过既有的属性,计算上一层的属性,一步步往上返回,获得线段树
wtevTree pushUp(wtevTree leftT, wtevTree rightT) {
// 新子段的lSum等于左区间的lSum或者左区间的 区间和 加上右区间的lSum
int l = Math.max(leftT.lSum, leftT.iSum + rightT.lSum);
// 新子段的rSum等于右区间的rSum或者右区间的 区间和 加上左区间的rSum
int r = Math.max(leftT.rSum + rightT.iSum, rightT.rSum);
// 新子段的区间和等于左右区间的区间和之和
int i = leftT.iSum + rightT.iSum;
// 新子段的最大子段和,其子段有可能穿过左右区间,或左区间,或右区间
int m = Math.max(leftT.rSum + rightT.lSum, Math.max(leftT.mSum, rightT.mSum));
return new wtevTree(l, r, i, m);
}

// 递归建立和获得输入区间所有子段的结构
wtevTree getInfo(int[] nums, int left, int right) {
if (left == right) // 若区间长度为1,其四个子段均为其值
return new wtevTree(nums[left], nums[left], nums[left], nums[left]);
int mid = (left + right) >> 1;// 获得中间点mid,右移一位相当于除以2,运算更快
wtevTree leftT = getInfo(nums, left, mid);
wtevTree rightT = getInfo(nums, mid + 1, right);//mid+1,左右区间没有交集。
return pushUp(leftT, rightT);//递归结束后,做最后一次合并
}
}

3.9 合并两个有序链表

21. 合并两个有序链表 - 力扣(Leetcode)

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

img
img
1
2
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

3.9.1 方法一:迭代法

没什么说的,创建一个新节点head,然后连接上就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode head = new ListNode(0);
ListNode p = head;
while(list1 != null && list2 != null){
if(list1.val <= list2.val){
p.next = list1;
list1 = list1.next;
}else{
p.next = list2;
list2 = list2.next;
}
p = p.next;
}
if(list1 == null){
p.next = list2;
}else{
p.next = list1;
}
return head.next;
}
}

3.9.2 方法二:递归法

递归连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//终止条件
if(list1 == null){
return list2;
}
//终止条件
if(list2 == null){
return list1;
}
if(list1.val <= list2.val){
list1.next = mergeTwoLists(list1.next, list2);
return list1;
}else{
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
}

3.10 两数之和

1. 两数之和 - 力扣(Leetcode)

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

进阶:你可以想出一个时间复杂度小于 O($n^2$)的算法吗?

1
2
3
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

3.10.1 方法一:暴力双循环

没什么好说的,暴力双循环,时间复杂度O($n^2$)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] ans = new int[2];
for(int i = 0; i < nums.length; i++){
for(int j = i + 1; j < nums.length; j++){
if(nums[i] + nums[j] == target){
ans[0] = i;
ans[1] = j;
}
}
}
return ans;
}
}

3.10.2 方法二:HashMap

因为结果要求返回的是下标,所以不能用排序改变元素位置。

只能通过HashMap来保存值和索引位置,map的key用来保存target - nums[i]的值,这样一来,只需要看后面的元素是否有等于这个差值的。

如果有的话,那就说明找到了这两个值的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] ans = new int[2];
HashMap<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
int temp = target - nums[i];
if(!map.containsKey(nums[i])){
map.put(temp, i);
}else{
ans[0] = i;
ans[1] = map.get(nums[i]);
break;
}
}
return ans;
}
}

时间复杂度为O(n)。

四、单例模式

4.1 懒汉式单例

特点:当需要使用对象的时候才进行实例化,需要考虑线程安全的问题,因此要加锁,用时间换空间。

传统实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton{
// 私有构造函数
private Singleton() {}

private static Singleton LazyMan;

// 加锁保证LazyMan只实例化一次,时间换空间
public static synchronized Singleton getInstance(){
if (LazyMan == null) {
LazyMan = new Singleton();
}
return LazyMan;
}
}

优化实现:

传统实现方式中,每次获取实例都要被synchronized关键字串行化(即使已经生成了实例)。

而我们加锁的目的是为了防止生成多个实例,因此只需对生成实例的代码加锁,生成实例后,可支持并发访问,提高了性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Singleton {
// 私有构造函数
private Singleton() {}
// volatile关键字禁止指令重排,如果不加,可能会出现return为null的情况
private volatile static Singleton LazyMan;

public static Singleton getInstance(){
// 已有实例则直接返回,不走锁
if (LazyMan == null) {
// 仅在没生成实例时加锁控制,使并发访问串行化
synchronized (Singleton.class) {
// 多个线程会按序执行到此处,需要再次检查是否已实例化
if (LazyMan == null) {
LazyMan = new Singleton();
}
}
}
return LazyMan;
}
}

由于检查了两次对象是否已实例化,该方法又称“双检锁”,能够同时保证性能及线程安全。

4.2 饿汉式单例

特点:类加载时便实例化对象,拿空间换时间。

传统实现:

1
2
3
4
5
6
7
8
9
10
class Singleton{
// 私有构造函数
private Singleton() {}
// 类加载时就实例化对象
private static Singleton Hungry = new Singleton();

public static Singleton getInstance(){
return Hungry;
}
}

优化实现:

传统实现方式中,由于类加载时就实例化对象,因此当我们调用静态方法时,也会进行实例化,从而导致空间的浪费。

由于静态内部类中的对象不会默认加载,直到调用了该内部类的方法,因此可用静态内部类封装静态实例变量

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton{
// 私有构造函数
private Singleton() {}

// 静态内部类
private static class SingletonHolder {
private static Singleton Hungry = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.Hungry;
}
}

五、多线程异步打印ABC

5.1 synchronized + wait/notify

5.1.1 三个线程轮流打印A、B、C,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
25
26
27
28
29
30
31
32
public class Main {
private int num;
private static Object Lock = new Object();

private void printABC(String name, int targetNum){
synchronized (Lock){
while (num % 3 != targetNum){
try {
Lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.print(name);
Lock.notifyAll();
}
}

public static void main(String[] args) {
Main main = new Main();
new Thread(() -> {
main.printABC("A", 0);
}, "ThreadA").start();
new Thread(() -> {
main.printABC("B", 1);
}, "ThreadB").start();
new Thread(() -> {
main.printABC("C", 2);
}, "ThreadC").start();
}
}

基本思路:

  1. 让A、B、C三个线程同时启动,因为num的初始值是0,所以线程B、C拿到Lock之后会进入循环;
  2. 然后B、C线程调用wait()方法进入等待;
  3. A线程不会进入循环,所以可以让num++,并且打印A;
  4. 打印完后,执行notifyAll()方法,让其他阻塞的线程苏醒,然后继续循环打印。

5.1.2 三个线程轮流打印A、B、C,10次

整体思路与上面一样,只不过在外面加了个循环而已。

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
30
31
32
33
34
public class Main {
private int num;
private static Object Lock = new Object();

private void printABC(String threadName, int targetNum){
for (int i = 0; i < 10; i++) {
synchronized (Lock){
while (num % 3 != targetNum){
try {
Lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.print(threadName);
Lock.notifyAll();
}
}
}

public static void main(String[] args) {
Main main = new Main();
new Thread(() -> {
main.printABC("A", 0);
}, "ThreadA").start();
new Thread(() -> {
main.printABC("B", 1);
}, "ThreadB").start();
new Thread(() -> {
main.printABC("C", 2);
}, "ThreadC").start();
}
}

5.1.3 两个线程轮流打印1-20内的奇偶数

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
30
31
32
33
public class Main {
private int num = 1;
private static Object Lock = new Object();

private void printOddEven(){
synchronized (Lock){
while (num <= 20){
//线程Odd先拿到锁
System.out.print(Thread.currentThread().getName() + ":");
System.out.println(num++);
try {
Lock.notifyAll(); //唤醒Even线程
Lock.wait(); //阻塞Odd线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Lock.notifyAll();
}
}

public static void main(String[] args) throws InterruptedException {
Main main = new Main();
new Thread(() -> {
main.printOddEven();
}, "Odd").start();
//确保Odd线程比Even线程先拿到锁
Thread.sleep(10);
new Thread(() -> {
main.printOddEven();
}, "Even").start();
}
}

5.1.4 N个线程轮流打印1-maxNum内的数

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Main {
private int num;
private static Object Lock = new Object();
private int maxNum = 20;
private int N = 4;

private void printMaxNum(int targetNum){
while (true){
synchronized (Lock){
while (num % N != targetNum){
if (num >= maxNum){
break;
}
try {
Lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (num >= maxNum){
break;
}
num++;
System.out.println(Thread.currentThread().getName() + ":" + num);
Lock.notifyAll();
}
}
}

public static void main(String[] args) throws InterruptedException {
Main main = new Main();
new Thread(() -> {
main.printMaxNum(0);
}, "T1").start();
new Thread(() -> {
main.printMaxNum(1);
}, "T2").start();
new Thread(() -> {
main.printMaxNum(2);
}, "T3").start();
new Thread(() -> {
main.printMaxNum(3);
}, "T4").start();
}
}

5.2 join()

join()方法可以指定线程执行的顺序,无论谁先执行,最后的顺序都是1-2-3。

还是那道题,三个线程轮流打印ABC十次

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
30
31
32
33
public class Main {
static class printABC implements Runnable{
private Thread beforeThread;
public printABC(Thread beforeThread){
this.beforeThread = beforeThread;
}

@Override
public void run() {
//判断前面是否有线程
if (beforeThread != null){
try {
beforeThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName());
}
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread T1 = new Thread(new printABC(null), "A");
Thread T2 = new Thread(new printABC(T1), "B");
Thread T3 = new Thread(new printABC(T2), "C");
T1.start();
T2.start();
T3.start();
Thread.sleep(10); //保证三个为一组
}
}
}

5.3 Lock

该方法更容易理解,不管哪个线程拿到锁,只有符合条件的才能打印。

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
public class Main {
private static int num;
private Lock lock = new ReentrantLock();

private void printABC(int targetNum){
for (int i = 0; i < 10; ) {
lock.lock();
if (num % 3 == targetNum){
System.out.print(Thread.currentThread().getName());
num++;
i++;
}
lock.unlock();
}
}

public static void main(String[] args){
Main main = new Main();
new Thread(() -> {
main.printABC(0);
}, "A").start();
new Thread(() -> {
main.printABC(1);
}, "B").start();
new Thread(() -> {
main.printABC(2);
}, "C").start();
}
}

六、AMC模式输入代码

6.1 数组建链表

  • 第一行输入n,表示链表中的元素个数;
  • 第二行输入n个元素,表示链表中节点的值;
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
30
31
32
33
34
35
36
class ListNode{
int value;
ListNode next;
public ListNode(){};
public ListNode(int value){
this.value = value;
}
}

public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
ListNode head = buildList(nums);
//打印链表
while (head != null){
System.out.print(head.value);
head = head.next;
}
}
//根据数组建立链表
public static ListNode buildList(int[] arr){
ListNode head = new ListNode(-1);
ListNode p = head;
for (int i = 0; i < arr.length; i++) {
ListNode q = new ListNode(arr[i]);
p.next = q;
p = p.next;
}
return head.next;
}
}

6.2 数组建二叉树

  • 第一行输入n,二叉树中一共有多少个节点;
  • 第二行输入n个元素,表示层序遍历二叉树的结果,-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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class TreeNode{
int value;
TreeNode left;
TreeNode right;

public TreeNode(int value){
this.value = value;
}
}

public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
TreeNode root = buildTree(nums);
}

//输入数组建立二叉树
public static TreeNode buildTree(int[] arr){
if (arr == null || arr.length == 0){
return null;
}
TreeNode root = new TreeNode(arr[0]);
//借助队列来实现
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);

//从数组中两个两个一读,代表该节点的左右节点
for (int i = 1; i < arr.length; i += 2) {
//从队列中出队,拼接该节点的左右子节点
TreeNode node = queue.poll();
//连上该节点的左子节点
if (arr[i] != -1){
node.left = new TreeNode(arr[i]);
//将新加的节点入队
queue.offer(node.left);
}
//连上该节点的右子节点,这里一定要先保证i + 1不会越界
if (i + 1 < arr.length && arr[i + 1] != -1){
node.right = new TreeNode(arr[i + 1]);
queue.offer(node.right);
}
}
return root;
}
}

6.3 输入边建二叉树

  • 第一行输入一个正整数n,代表节点的数量;
  • 接下来的n - 1行,每行输入两个正整数a,b,代表节点a和节点b有一条边连接;
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
30
31
32
class TreeNode{
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val){
this.val = val;
}
}

public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
// 创建一个数组来保存每个节点的引用
TreeNode[] nodes = new TreeNode[n + 1];
for (int i = 1; i <= n; i++) {
nodes[i] = new TreeNode(i);
}
for (int i = 0; i < n - 1; i++) {
int u = scanner.nextInt();
int v = scanner.nextInt();
// 根据输入的边关系连接节点
if (nodes[u].left == null) {
nodes[u].left = nodes[v];
} else {
nodes[u].right = nodes[v];
}
}
// 假设根节点是节点1
TreeNode root = nodes[1];
}
}

6.4 输入边建二叉树(以链表形式存储)

这种方式存的是边,并没有真的建立二叉树。

  • 第一行输入一个正整数n,代表节点的数量;
  • 接下来的n - 1行,每行输入两个正整数a,b,代表节点a和节点b有一条边连接;
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
import java.util.*;

public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();

List<List<Integer>> edge = new ArrayList<>();
for (int i = 0; i <= n; i++) {
edge.add(new ArrayList<>());
}

for (int i = 0; i < n - 1; i++) {
int a = sc.nextInt();
int b = sc.nextInt();
edge.get(a).add(b);
edge.get(b).add(a);
}

for (int i = 0; i < edge.size(); i++) {
for (int j = 0; j < edge.get(i).size(); j++) {
System.out.print(edge.get(i).get(j) + " ");
}
System.out.println();
}
}
}

本站由 Cccccpg 使用 Stellar 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。