CS-Notes/docs/notes/剑指 Offer 题解 - 40~49.md
2019-06-09 22:32:10 +08:00

424 lines
13 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- GFM-TOC -->
* [40. 最小的 K 个数](#40-最小的-k-个数)
* [41.1 数据流中的中位数](#411-数据流中的中位数)
* [41.2 字符流中第一个不重复的字符](#412-字符流中第一个不重复的字符)
* [42. 连续子数组的最大和](#42-连续子数组的最大和)
* [43. 1 n 整数中 1 出现的次数](#43--1--n-整数中-1-出现的次数)
* [44. 数字序列中的某一位数字](#44-数字序列中的某一位数字)
* [45. 把数组排成最小的数](#45-把数组排成最小的数)
* [46. 把数字翻译成字符串](#46-把数字翻译成字符串)
* [47. 礼物的最大价值](#47-礼物的最大价值)
* [48. 最长不含重复字符的子字符串](#48-最长不含重复字符的子字符串)
* [49. 丑数](#49-丑数)
<!-- GFM-TOC -->
# 40. 最小的 K 个数
[NowCoder](https://www.nowcoder.com/practice/6a296eb82cf844ca8539b57c23e6e9bf?tpId=13&tqId=11182&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
## 解题思路
### 快速选择
- 复杂度O(N) + O(1)
- 只有当允许修改数组元素时才可以使用
快速排序的 partition() 方法会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j] a[j+1..h] 大于等于 a[j]此时 a[j] 就是数组的第 j 大元素可以利用这个特性找出数组的第 K 个元素这种找第 K 个元素的算法称为快速选择算法
```java
public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
ArrayList<Integer> ret = new ArrayList<>();
if (k > nums.length || k <= 0)
return ret;
findKthSmallest(nums, k - 1);
/* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */
for (int i = 0; i < k; i++)
ret.add(nums[i]);
return ret;
}
public void findKthSmallest(int[] nums, int k) {
int l = 0, h = nums.length - 1;
while (l < h) {
int j = partition(nums, l, h);
if (j == k)
break;
if (j > k)
h = j - 1;
else
l = j + 1;
}
}
private int partition(int[] nums, int l, int h) {
int p = nums[l]; /* 切分元素 */
int i = l, j = h + 1;
while (true) {
while (i != h && nums[++i] < p) ;
while (j != l && nums[--j] > p) ;
if (i >= j)
break;
swap(nums, i, j);
}
swap(nums, l, j);
return j;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
```
### 大小为 K 的最小堆
- 复杂度O(NlogK) + O(K)
- 特别适合处理海量数据
应该使用大顶堆来维护最小堆而不能直接创建一个小顶堆并设置一个大小企图让小顶堆中的元素都是最小元素
维护一个大小为 K 的最小堆过程如下在添加一个元素之后如果大顶堆的大小大于 K那么需要将大顶堆的堆顶元素去除
```java
public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
if (k > nums.length || k <= 0)
return new ArrayList<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
for (int num : nums) {
maxHeap.add(num);
if (maxHeap.size() > k)
maxHeap.poll();
}
return new ArrayList<>(maxHeap);
}
```
# 41.1 数据流中的中位数
[NowCoder](https://www.nowcoder.com/practice/9be0172896bd43948f8a32fb954e1be1?tpId=13&tqId=11216&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
## 题目描述
如何得到一个数据流中的中位数如果从数据流中读出奇数个数值那么中位数就是所有数值排序之后位于中间的数值如果从数据流中读出偶数个数值那么中位数就是所有数值排序之后中间两个数的平均值
## 解题思路
```java
/* 大顶堆,存储左半边元素 */
private PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1);
/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */
private PriorityQueue<Integer> right = new PriorityQueue<>();
/* 当前数据流读入的元素个数 */
private int N = 0;
public void Insert(Integer val) {
/* 插入要保证两个堆存于平衡状态 */
if (N % 2 == 0) {
/* N 为偶数的情况下插入到右半边。
* 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大,
* 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */
left.add(val);
right.add(left.poll());
} else {
right.add(val);
left.add(right.poll());
}
N++;
}
public Double GetMedian() {
if (N % 2 == 0)
return (left.peek() + right.peek()) / 2.0;
else
return (double) right.peek();
}
```
# 41.2 字符流中第一个不重复的字符
[NowCoder](https://www.nowcoder.com/practice/00de97733b8e4f97a3fb5c680ee10720?tpId=13&tqId=11207&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
## 题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符例如当从字符流中只读出前两个字符 "go" 第一个只出现一次的字符是 "g"当从该字符流中读出前六个字符google" 时,第一个只出现一次的字符是 "l"
## 解题思路
```java
private int[] cnts = new int[256];
private Queue<Character> queue = new LinkedList<>();
public void Insert(char ch) {
cnts[ch]++;
queue.add(ch);
while (!queue.isEmpty() && cnts[queue.peek()] > 1)
queue.poll();
}
public char FirstAppearingOnce() {
return queue.isEmpty() ? '#' : queue.peek();
}
```
# 42. 连续子数组的最大和
[NowCoder](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
## 题目描述
{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8从第 0 个开始,到第 3 个为止)。
## 解题思路
```java
public int FindGreatestSumOfSubArray(int[] nums) {
if (nums == null || nums.length == 0)
return 0;
int greatestSum = Integer.MIN_VALUE;
int sum = 0;
for (int val : nums) {
sum = sum <= 0 ? val : sum + val;
greatestSum = Math.max(greatestSum, sum);
}
return greatestSum;
}
```
# 43. 从 1 到 n 整数中 1 出现的次数
[NowCoder](https://www.nowcoder.com/practice/bd7f978302044eee894445e244c7eee6?tpId=13&tqId=11184&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
## 解题思路
```java
public int NumberOf1Between1AndN_Solution(int n) {
int cnt = 0;
for (int m = 1; m <= n; m *= 10) {
int a = n / m, b = n % m;
cnt += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0);
}
return cnt;
}
```
> [Leetcode : 233. Number of Digit One](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython)
# 44. 数字序列中的某一位数字
## 题目描述
数字以 0123456789101112131415... 的格式序列化到一个字符串中,求这个字符串的第 index 位。
## 解题思路
```java
public int getDigitAtIndex(int index) {
if (index < 0)
return -1;
int place = 1; // 1 表示个位2 表示 十位...
while (true) {
int amount = getAmountOfPlace(place);
int totalAmount = amount * place;
if (index < totalAmount)
return getDigitAtIndex(index, place);
index -= totalAmount;
place++;
}
}
/**
* place 位数的数字组成的字符串长度
* 10, 90, 900, ...
*/
private int getAmountOfPlace(int place) {
if (place == 1)
return 10;
return (int) Math.pow(10, place - 1) * 9;
}
/**
* place 位数的起始数字
* 0, 10, 100, ...
*/
private int getBeginNumberOfPlace(int place) {
if (place == 1)
return 0;
return (int) Math.pow(10, place - 1);
}
/**
* 在 place 位数组成的字符串中,第 index 个数
*/
private int getDigitAtIndex(int index, int place) {
int beginNumber = getBeginNumberOfPlace(place);
int shiftNumber = index / place;
String number = (beginNumber + shiftNumber) + "";
int count = index % place;
return number.charAt(count) - '0';
}
```
# 45. 把数组排成最小的数
[NowCoder](https://www.nowcoder.com/practice/8fecd3f8ba334add803bf2a06af1b993?tpId=13&tqId=11185&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
## 题目描述
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {332321},则打印出这三个数字能排成的最小数字为 321323。
## 解题思路
可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1那么应该把 S1 排在前面,否则应该把 S2 排在前面。
```java
public String PrintMinNumber(int[] numbers) {
if (numbers == null || numbers.length == 0)
return "";
int n = numbers.length;
String[] nums = new String[n];
for (int i = 0; i < n; i++)
nums[i] = numbers[i] + "";
Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1));
String ret = "";
for (String str : nums)
ret += str;
return ret;
}
```
# 46. 把数字翻译成字符串
[Leetcode](https://leetcode.com/problems/decode-ways/description/)
## 题目描述
给定一个数字按照如下规则翻译成字符串1 翻译成“a”2 翻译成“b”... 26 翻译成“z”。一个数字有多种翻译可能例如 12258 一共有 5 种,分别是 abbehlbehavehabyhlyh。实现一个函数用来计算一个数字有多少种不同的翻译方法。
## 解题思路
```java
public int numDecodings(String s) {
if (s == null || s.length() == 0)
return 0;
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = s.charAt(0) == '0' ? 0 : 1;
for (int i = 2; i <= n; i++) {
int one = Integer.valueOf(s.substring(i - 1, i));
if (one != 0)
dp[i] += dp[i - 1];
if (s.charAt(i - 2) == '0')
continue;
int two = Integer.valueOf(s.substring(i - 2, i));
if (two <= 26)
dp[i] += dp[i - 2];
}
return dp[n];
}
```
# 47. 礼物的最大价值
[NowCoder](https://www.nowcoder.com/questionTerminal/72a99e28381a407991f2c96d8cb238ab)
## 题目描述
在一个 m\*n 的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于 0。从左上角开始拿礼物每次向右或向下移动一格直到右下角结束。给定一个棋盘求拿到礼物的最大价值。例如对于如下棋盘
```
1 10 3 8
12 2 9 6
5 7 4 11
3 7 16 5
```
礼物的最大价值为 1+12+5+7+7+16+5=53。
## 解题思路
应该用动态规划求解,而不是深度优先搜索,深度优先搜索过于复杂,不是最优解。
```java
public int getMost(int[][] values) {
if (values == null || values.length == 0 || values[0].length == 0)
return 0;
int n = values[0].length;
int[] dp = new int[n];
for (int[] value : values) {
dp[0] += value[0];
for (int i = 1; i < n; i++)
dp[i] = Math.max(dp[i], dp[i - 1]) + value[i];
}
return dp[n - 1];
}
```
# 48. 最长不含重复字符的子字符串
## 题目描述
输入一个字符串(只包含 a\~z 的字符),求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr最长不含重复字符的子字符串为 acfr长度为 4。
## 解题思路
```java
public int longestSubStringWithoutDuplication(String str) {
int curLen = 0;
int maxLen = 0;
int[] preIndexs = new int[26];
Arrays.fill(preIndexs, -1);
for (int curI = 0; curI < str.length(); curI++) {
int c = str.charAt(curI) - 'a';
int preI = preIndexs[c];
if (preI == -1 || curI - preI > curLen) {
curLen++;
} else {
maxLen = Math.max(maxLen, curLen);
curLen = curI - preI;
}
preIndexs[c] = curI;
}
maxLen = Math.max(maxLen, curLen);
return maxLen;
}
```
# 49. 丑数
[NowCoder](https://www.nowcoder.com/practice/6aa9e04fc3794f68acf8778237ba065b?tpId=13&tqId=11186&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking)
## 题目描述
把只包含因子 2、3 和 5 的数称作丑数Ugly Number。例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。
## 解题思路
```java
public int GetUglyNumber_Solution(int N) {
if (N <= 6)
return N;
int i2 = 0, i3 = 0, i5 = 0;
int[] dp = new int[N];
dp[0] = 1;
for (int i = 1; i < N; i++) {
int next2 = dp[i2] * 2, next3 = dp[i3] * 3, next5 = dp[i5] * 5;
dp[i] = Math.min(next2, Math.min(next3, next5));
if (dp[i] == next2)
i2++;
if (dp[i] == next3)
i3++;
if (dp[i] == next5)
i5++;
}
return dp[N - 1];
}
```
<img width="580px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/other/公众号海报1.png"></img>