66. 加一
题目链接
标签
数组 数学
思路
本题十分简单,如果会 高精度 的解决方法,则本题更是小菜一碟。核心思想是:给低位加一,如果低位小于 10
,则可以直接返回;否则就将低位的 10
变成 0
,然后继续判断更高位,直到将整个数组都遍历完;如果遍历完整个数组都没有返回,则说明 最高位 的值为 10
,此时需要构建一个新的数字返回,新的数字的长度为 原数字的长度 加 一,且最高位为一。
解释:
- 为何将低位的
10
变成0
?:由于是加一操作,所以某一位上加一得到的最大数字是10
,即9 + 1
,此时让10
对10
取余就是0
,是一个确定的数,为了避免取余操作浪费时间,直接赋值0
。 - 为何遍历完整个数组还没有返回?:例如
9, 99, 999
这样的数,遍历完整个数组后最高位都是10
。而不是小于10
的数,所以没有在循环中返回,而是需要构建新的数字进行返回。
代码
class Solution {
public int[] plusOne(int[] digits) {
int n = digits.length;
for (int i = n - 1; i >= 0; i--) { // 从 最低位 到 最高位 遍历
digits[i]++; // 给这一位加一
if (digits[i] < 10) { // 如果这一位加上一的结果小于 10
return digits; // 则无需对更高位进行加一的操作,直接返回结果即可
}
digits[i] = 0; // 否则需要对更高位进行加一的操作,先将这一位的 10 变成 0
}
// 如果能到这一步,则说明 最高位 的值为 10,其余位都是 0
digits = new int[n + 1]; // 构建一个 n + 1 位的数字
digits[0] = 1; // 让最高位为 1
return digits; // 返回构建的新数
}
}
169. 多数元素
题目链接
标签
数组 哈希表 分治 计数 排序
法一:计数
思路
本题可以使用最简单的思路来解决,使用 Map
统计数组中每个数字出现的次数,然后选出出现次数最多的数字,这个数字就是题目中要求的 出现次数大于 n / 2
的数字。
代码
class Solution {
public int majorityElement(int[] nums) {
// 统计每个数字出现的次数,key 为数字,value 为其出现的次数
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// 在整个 Map 中选出出现次数最多的数字
Map.Entry<Integer, Integer> resEntry = null;
for (Map.Entry<Integer, Integer> entry : count.entrySet()) {
if (resEntry == null || resEntry.getValue() < entry.getValue()) {
resEntry = entry;
}
}
return resEntry.getKey();
}
}
法二:排序
思路
如果将数组排序,那么出现次数大于 n / 2
的数字势必会出现在数组的中间位置。例如对于数组 nums = [2, 2, 1, 1, 1, 2, 2]
,它排序的结果为 [1, 1, 1, 2, 2, 2, 2]
,数字 2
正好在数组的中间位置上。
以下是对这个结论的文字论证:
- 如果所求数字是数组中的最值(最小值 或 最大值),那么它肯定会有一部分位于数组的左子区间,另一部分位于数组的右子区间,所以数组的中间位置必定是所求的数字。例如:
[1, 1, 1, 1, 2, 2, 2]
,[1, 1, 1, 2, 2, 2, 2]
。 - 如果所求数字不是数组中的最值,那么它的两边会有其他值,但是仍然有一部分位于数组的左子区间,另一部分位于数组的右子区间,所以数组的中间位置还是所求的数字。例如:
[1, 2, 2, 2, 2, 3, 4]
。
代码
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums); // 排序
return nums[nums.length / 2]; // 获取中间的数字
}
}
215. 数组中的第K个最大元素
题目链接
标签
数组 分治 快速选择 排序 堆(优先队列)
思路
本题可以维护一个长度为 k
的小顶堆,数值越小,越靠近顶部,将数组中所有的元素都“添加”到优先队列中,注意此处的“添加”不是真正的添加,需要分情况讨论:
- 对于数组的前
k
个元素,直接 添加 即可。 - 对于数组的剩余元素,先判断它是否大于堆顶元素,如果大于,则用它 替换 堆顶元素。
这样一来,小顶堆中就存储着数组中 从 第 1
大 到 第 k
大 的元素,并且堆顶是这些元素中最小的元素,即第 k
大的元素。
如果对 小顶堆、大顶堆、优先队列 不熟悉,可以看 数据结构——优先队列,本文使用大顶堆实现了优先队列,理解大顶堆实现的优先队列后,也就能理解小顶堆的实现了,区别只有 上浮操作 和 下潜操作 的判断。
代码
class Solution {
public int findKthLargest(int[] nums, int k) {
MinHeap heap = new MinHeap(k); // 构建一个长度为 k 的小顶堆
// 先将数组中的前 k 个数加入小顶堆
for (int i = 0; i < k; i++) {
heap.offer(nums[i]);
}
for (int i = k; i < nums.length; i++) { // 对于剩余的数
if (heap.peek() < nums[i]) { // 如果 剩余的数 大于 小顶堆中最小的数
heap.replace(nums[i]); // 则使用 剩余的数 替换 小顶堆中最小的数
}
}
return heap.peek(); // 返回堆顶元素
}
private static class MinHeap { // 小顶堆
public MinHeap(int capacity) {
data = new int[capacity];
}
// 添加新数
public void offer(int value) {
int child = up(value);
data[child] = value;
size++;
}
// 将原先堆顶的数字替换成 newValue
public void replace(int newValue) {
data[0] = newValue; // 先替换
down(0); // 后将其下潜到合适的位置
}
// 取出最小的数
public int peek() {
return data[0];
}
// 上浮操作
private int up(int value) {
int child = size;
int parent = getParent(child);
// 类似 插入排序
while (child > 0
&& value < data[parent]) { // 只有 当前数 小于 父节点的数 才进行“交换”
data[child] = data[parent];
child = parent;
parent = getParent(parent);
}
return child;
}
// 下潜操作
private void down(int parent) {
int left = getLeft(parent);
int right = left + 1;
int min = parent; // min 是 在父节点和两个子节点中,最小值的 索引
if (left < size && data[min] > data[left]) {
min = left;
}
if (right < size && data[min] > data[right]) {
min = right;
}
if (min == parent) {
return;
}
swap(min, parent);
down(min);
}
// 根据 子节点的索引 获取 父节点的索引
private int getParent(int child) {
return (child - 1) >> 1;
}
// 根据 父节点的索引 获取 左子节点的索引
private int getLeft(int parent) {
return (parent << 1) + 1;
}
// 交换指定索引的两个元素
private void swap(int i, int j) {
int temp = data[j];
data[j] = data[i];
data[i] = temp;
}
private int[] data;
private int size;
}
}