有序数组的单一元素
题目
版本1 正确
public int singleNonDuplicate(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
// 在有序数组中, 寻找只出现一次的元素, 其它元素出现两次
// 有序数组, 一定要考虑二分法
// 数组一定为奇数个
// 寻找数组中间的元素, 如果该元素和前后元素都不相等, 那么就是我们要寻找的,
// 如果该元素和前面一个或者后面一个的元素相等, 那么就可以排除另一半的数组
return digui(nums, 0, nums.length - 1);
}
public int digui(int [] nums, int start, int end) {
if (end == start) {
return nums[start];
}
int mid = start + (end - start) / 2;
// mid将start到end这一部分数组分成了两部分, 两部分可能都是奇数, 可能都是偶数
if ((mid - start) % 2 == 0) {
// 如果两边是偶数
if (nums[mid] == nums[mid - 1]) {
// 舍去后面
return digui(nums, start, mid - 2);
} else if (nums[mid] == nums[mid + 1]) {
// 舍去前面
return digui(nums, mid + 2, end);
} else {
return nums[mid];
}
} else {
// 如果两边是奇数
if (nums[mid] == nums[mid - 1]) {
// 舍去前面
return digui(nums, mid + 1, end);
} else if (nums[mid] == nums[mid + 1]) {
// 舍去后面
return digui(nums, start, mid - 1);
} else {
return nums[mid];
}
}
}
正确的原因
(1) 注意mid将数组分成两部分, 需要根据所有两边剩余的是奇数还是偶数, 然后结合和哪边的元素相等, 才能判断应该舍弃哪部分, 应该怎么传递索引
在排序数组中查找数字
题目
版本1 正确
public static int search(int[] nums, int target) {
if (nums.length == 0) {
return 0;
}
// 在升序数组中, 查找target出现的次数
// 因为重复的数字可能非常多, 因此使用两次二分查找, 反而效率更快
int left = findLeft(nums, target, 0, nums.length - 1);
int right = findRight(nums, target, 0, nums.length - 1);
return right - left + 1;
}
public static int findLeft(int [] nums, int target, int start, int end) {
// 注意左侧边界需要返回start, 并且是 > 才终止
if (start > end) {
return start;
}
// 寻找左侧边界
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
// 逐渐逼近左边界
return findLeft(nums, target, start, mid - 1);
} else if (nums[mid] > target) {
return findLeft(nums, target, start, mid - 1);
} else {
return findLeft(nums, target, mid + 1, end);
}
}
public static int findRight(int [] nums, int target, int start, int end) {
// 注意右侧边界需要返回end, 并且是 > 才终止
if (start > end) {
return end;
}
// 寻找右侧边界
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
// 逐渐逼近右边界
return findRight(nums, target, mid + 1, end);
} else if (nums[mid] > target) {
return findRight(nums, target, start, mid - 1);
} else {
return findRight(nums, target, mid + 1, end);
}
}
调整数组顺序使奇数位于偶数前面
题目
版本1 正确
public static int[] exchange(int[] nums) {
// 类似快排双指针
int left = 0;
int right = nums.length - 1;
while (left <= right) {
while (right > 0 && nums[right] % 2 == 0) {
right --;
}
while (left < nums.length && nums[left] % 2 != 0) {
left ++;
}
// 交换一下left和right
if (left <= right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
right --;
left ++;
}
}
return nums;
}
正确的原因
(1) 右边指针一直移动的时候, 要注意边界, 左边指针同理
(2) 元素发生交换需要满足条件left <= right
数组中出现次数超过一半的数字
题目
版本1 正确
public int majorityElement(int[] nums) {
// 求数组中超过一半的元素
int vote = 0;
int target = 0;
for (int i = 0; i < nums.length; i ++) {
if (vote == 0) {
vote ++;
target = nums[i];
} else if (target == nums[i]) {
vote ++;
} else {
vote --;
}
if (vote > nums.length / 2) {
break;
}
}
return target;
}
正确的原因
(1) 投票法, 如果vote为0, 认为下一个数是众数, 如果遇见相同的数字, 则vote ++, 否则vote -- 最后的数字, 一定就是出现超过一半的数字.
二维数组的查找
题目
版本1 正确
public static boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix.length == 0 || matrix[0].length == 0) {
return Boolean.FALSE;
}
if (target < matrix[0][0] || target > matrix[matrix.length - 1][matrix[0].length - 1]) {
return Boolean.FALSE;
}
// 从数组右上角开始寻找
int i = 0;
int j = matrix[0].length - 1;
while (i < matrix.length && j>= 0) {
if (matrix[i][j] == target) {
return Boolean.TRUE;
} else if (matrix[i][j] > target) {
// 左移
j --;
} else {
i ++;
}
}
return Boolean.FALSE;
}
正确的原因
(1) 注意这个二维数组 可以使用二分查找提高查找速度, 但是其实本身不用, 因为从右上角开始遍历, 是可以确定遍历步骤的.
把数组排成最小的数
题目
版本1 正确
public static String minNumber(int[] nums) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
if ((String.valueOf(o1) + String.valueOf(o2)).compareTo(String.valueOf(o2) + String.valueOf(o1)) > 0) {
// 返回1就代表交换, 这里o1 > o2, 所以返回1, 让o1和o2交换
return 1;
} else {
return -1;
}
}
});
// priorityQueue此时得到的是小顶堆, 最小值在队列头部
for (int i = 0; i < nums.length; i ++) {
priorityQueue.offer(nums[i]);
}
// 元素全部添加完毕后, 就得到了排序的结果
StringBuilder sb = new StringBuilder();
for (int i = 0; i < nums.length; i ++) {
sb.append(String.valueOf(priorityQueue.poll()));
}
return sb.toString();
}
正确的原因
(1) 其实本质还是给数组里的元素排序, 只不过排序的依据换成了根据字典序列进行排序.
(2) compartor的返回值, 返回>0的时候, 就代表让o1和o2交换, 这样思路就明确了. 当o1 > o2的时候, 返回1, 得到的就是小顶堆, 当o2 > o1的时候, 返回1, 就是大顶堆, 默认就是大顶堆.
数字序列中某一位数字
题目
版本1 正确
public int findNthDigit(int n) {
// 求第n位第数字
// 首先计算n所在的位数范围
long start = 1;
int digit = 1;
long count = 9;
while (n > count) {
n -= count;
start = start * 10;
digit = digit + 1;
count = start * 9 * digit;
}
// n - 1是为了排除0
// num就是n所在的那个数字
long num = start + (n - 1) / digit;
// 返回n具体在哪一位
return Long.toString(num).charAt((n - 1) % digit) - '0';
}
正确的原因
(1) 明确思路, 不同位数拥有的数量是有关系的, 可以逐步计算n属于哪个数字, 然后再计算n到底对应的是哪一位
(2) 例如start和count应该使用long类型, digit使用int, 在计算n -= count;的时候, 一定要这么写, 不能写成 n = n - count;
旋转数组最小的数字
题目
版本1 正确
public int minArray(int[] numbers) {
// 数组部分有序的, 利用二分法在数组中寻找最小值
// 排序数组旋转一次后, 最小值一定在右侧排序数组
// 即右侧排序数组的最大值, 等于左侧排序数组的最小值
// 每次比较mid和right的值, 不比较mid和left是因为当numbers[mid] < numbers[left]的时候, 无法判断应该在哪个区间
int left = 0;
int right = numbers.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (numbers[mid] > numbers[right]) {
// 说明mid处于左侧的有序数组, 最小值一定在mid右侧
left = mid + 1;
} else if (numbers[mid] < numbers[right]) {
// mid位于右侧的有序数组, 那么mid到right一定是递增的, 最小值就为mid, 或者在mid左侧
right = mid;
} else {
// 如果相等, 只能证明right节点是一定没用的, 其它的不能证明
right --;
}
}
return numbers[left];
}
正确的原因
(1) 只旋转一次的数组, 有一个特点, 左侧有序数组的最小值, 是大于等于右侧有序数组的最大值的.
(2) 利用二分法, 每次比较mid和right的值, 可以逐渐缩小最小值所在的范围
和为S的连续整数序列
题目
版本1 正确
public int[][] findContinuousSequence(int target) {
// 有序数组就是1....target - 1
// 结果要求是连续的元素
// 要连续子数组的和为某个值, 因为还需要得到具体数组, 而不是有多少的个数, 因此不采用前缀和
// 采用滑动窗口
int left = 1;
int right = 2;
// 因为不能是一个元素
List<int []> ans = new ArrayList<>();
while (left < right) {
int sum = (left + right) * (right - left + 1) / 2;
if (sum == target) {
int [] temp = new int[right - left + 1];
for (int i = 0; i < right - left + 1; i ++) {
temp[i] = left + i;
}
ans.add(temp);
// 平移
left ++;
right ++;
} else if (sum > target) {
left ++;
} else {
right ++;
}
}
int [][] res = new int[ans.size()][];
for (int i = 0; i < ans.size(); i ++) {
res[i] = ans.get(i);
}
return res;
}
正确的原因
(1) 因为是有序数组, 采用滑动窗口, 记录窗口范围里的和, 窗口范围里的和可以直接计算得到.
合并两个有序数组
题目
版本1 正确
public static void merge(int[] nums1, int m, int[] nums2, int n) {
// 合并两个有序数组, nums1数组中有足够的空间
// 因此我们可以在两个数组的尾巴开始, 挑选最大值放到nums1的最后.
// 这样就不用担心nums1数字被覆盖的问题了
int right1 = m - 1;
int right2 = n - 1;
for (int i = m + n - 1; i >=0; i --) {
if (right1 < 0) {
nums1[i] = nums1[right2];
right2 --;
} else if (right2 < 0) {
nums1[i] = nums1[right1];
right1 --;
} else if (nums1[right1] >= nums2[right2]) {
nums1[i] = nums1[right1];
right1 --;
} else {
nums1[i] = nums1[right2];
right2 --;
}
}
}
正确的原因
(1) 从两个数组中, 选出最大值, 放到nums1的末尾, 这样就不存在需要新建数组空间, 并且也不会需要移动nums1元素的问题了
有序数组中差绝对值之和
题目
版本1 正确
public int[] getSumAbsoluteDifferences(int[] nums) {
// nums是有序的
// 任何一个元素i将数组分成两部分, 前半部分都是<= nums[i]
// 后半部分 >= nums[i]
// 我得到前半部分的和, 用 n * nums[i] - sum
// 后半部分是sum - n * nums[i]
// 就得到了题目要求的结果
// 得到nums的前缀和
int [] prefixSum = new int[nums.length];
for (int i = 0; i < nums.length; i ++) {
if (i == 0) {
prefixSum[i] = nums[i];
} else {
prefixSum[i] = prefixSum[i - 1] + nums[i];
}
}
int [] ans = new int[nums.length];
for (int i = 0; i < nums.length; i ++) {
// 前半部分的和
int leftNum = i;
int leftSum = 0;
if (leftNum > 0) {
leftSum = leftNum * nums[i] - prefixSum[i - 1];
}
// 后半部分
int rightNum = nums.length - 1 - i;
int rightSum = 0;
if (rightNum > 0) {
rightSum = prefixSum[nums.length - 1] - prefixSum[i] - rightNum * nums[i];
}
ans[i] = leftSum + rightSum;
}
return ans;
}
正确的原因
(1) 首先是有序数组, i可以将前后分割成可以直接计算结果的数组
(2) 利用前缀和避免遍历
(3) 每次结果计算两次即可
删除有序数组中的重复项
题目
版本1 正确
public int removeDuplicates(int[] nums) {
// 采用快慢指针, 快指针比慢指针一开始多走一步, 如果快慢指针的值相同了, 都为A, 那么快指针继续走, 慢指针停下来
// 然后快慢指针不同时, 快指针为C, 赋值一次, 然后此时nums[slow] = C, 此时j如果继续遇见C, 依旧会跳过C
int slow = 0;
int fast = 1;
while (fast < nums.length) {
if (nums[fast] == nums[slow]) {
// slow停止, fast继续走
while (fast < nums.length && nums[fast] == nums[slow]) {
fast ++;
}
if (fast != nums.length) {
// fast遇见和slow不同的元素了, 并且fast没有走出边界
slow ++; // 一定要先slow++, 因为相同的元素需要保留一个
nums[slow] = nums[fast];
}
} else {
slow ++;
fast ++;
}
}
return slow + 1;
}
正确的原因
(1) 利用快慢指针, 快指针用来跳过重复的元素, 慢指针用来标示最后一个不重复的元素是啥.
(2) 思路挺巧妙的, 本身想着fast遇见新的重复元素该怎么办, 但是因为slow更新了, 所以fast不会受影响.
早餐组合
题目
版本1 正确
public int breakfastNumber(int[] staple, int[] drinks, int x) {
// 对食物数组进行计数排序, 统计每个食物值出现的次数
int [] arr = new int[x + 1];
for (int i = 0; i < staple.length; i ++) {
if (staple[i] <= x) {
arr[staple[i]] ++;
}
}
// 然后对arr求前缀和, 这样对于任意价格的食物, 都可以知道小于等于该价格的种类有多少
// 前缀和中记录的是次数
for (int i = 1; i < arr.length; i ++) {
arr[i] = arr[i] + arr[i - 1];
}
// 遍历一遍饮料, 统计结果
long count = 0;
for (int i = 0; i < drinks.length; i ++) {
int drink = drinks[i];
if (drink <= x) {
count += arr[x - drink];
}
}
int ans = (int) (count % 1000000007L);
return ans;
}
正确的原因
(1) 这个思路类似求连续数组和为S的子数组个数那道题, 都是利用前缀和, 计算符合条件的数据的次数.
(2) 注意方式吧, 下次记住
翻转单词顺序
题目
版本1 正确
public static String reverseWords(String s) {
if (s.length() == 0) {
return "";
}
// 到序遍历一遍字符串
// 遇见空格就跳过
StringBuilder sb = new StringBuilder();
int slow = s.length() - 1;
int fast = slow;
for (int i = s.length() - 1; i >=0; i --) {
if (s.charAt(i) == ' ') {
slow --;
fast --;
} else {
// slow停止 fast继续移动
while (fast >= 0 && s.charAt(fast) != ' ') {
fast --;
}
sb.append(s.substring(fast + 1, slow + 1));
sb.append(" ");
i = fast + 1;
slow = fast;
}
}
String ans = sb.toString();
if (ans.length() >= 2) {
return ans.substring(0, ans.length() - 1);
}
return ans;
}
正确的原因
(1) 为了翻转字符串, 可以倒着遍历, 没必要正着遍历再翻转
(2) i = fast + 1; i赋值的时候要注意, 本身for循环i就减少1
(3) 取出ans最后一个括号的时候, 要注意长度
前k个高频元素
题目
版本1 正确
public int[] topKFrequent(int[] nums, int k) {
if (k == 0 || nums.length == 0) {
return new int[0];
}
// 出现频率前k高的元素
// 利用map统计每个元素出现的次数, 然后利用堆找到频次前k大的元素
// 维护一个小顶堆
Map<Integer, Integer> num2FreqMap = new HashMap<>();
for (int i = 0; i < nums.length; i ++) {
num2FreqMap.put(nums[i], num2FreqMap.getOrDefault(nums[i], 0) + 1);
}
// 维护一个大小为k的小顶堆
PriorityQueue<int []> priorityQueue = new PriorityQueue<>(new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
// 维护小顶堆
return o1[1] - o2[1];
}
});
// 遍历所有元素
for(Map.Entry<Integer, Integer> entry : num2FreqMap.entrySet()) {
int [] data = new int[2];
data[0] = entry.getKey();
data[1] = entry.getValue();
// 添加入队列
if (priorityQueue.size() == k) {
// 和堆顶元素做比较
if (data[1] > priorityQueue.peek()[1]) {
priorityQueue.poll();
priorityQueue.offer(data);
}
} else {
// 队列小于k, 直接添加即可
priorityQueue.offer(data);
}
}
// 队列中最后剩下的就是频次前k的元素
int [] ans = new int[priorityQueue.size()];
for (int i = priorityQueue.size() - 1; i >= 0; i --) {
ans[i] = priorityQueue.poll()[0];
}
return ans;
}
正确的原因
(1) 利用hashMap统计次数, 维护一个小顶堆来记录频次前k的元素.
版本2 利用快排
// 注意全局变量用static, 在最后结果的时候会多统计
List<int []> ans = new ArrayList<>();
public int[] topKFrequent(int[] nums, int k) {
if (k == 0 || nums.length == 0) {
return new int[0];
}
// 出现频率前k高的元素
// 利用map找到统计每个元素出现的频次, 然后对于频次数组利用快排缩小问题规模
// 直到找到前k频次的数字
Map<Integer, Integer> nums2FreqMap = new HashMap<>();
for (int i = 0; i < nums.length; i ++) {
nums2FreqMap.put(nums[i], nums2FreqMap.getOrDefault(nums[i], 0) + 1);
}
// 构造频次数组
int [][] freqArray = new int[nums2FreqMap.size()][2];
int index = 0;
for (Map.Entry<Integer, Integer> entry : nums2FreqMap.entrySet()) {
int [] temp = new int[2];
temp[0] = entry.getKey();
temp[1] = entry.getValue();
freqArray[index] = temp;
index ++;
}
// 利用快排的思想来解决
quickSort(freqArray, 0, freqArray.length - 1, k);
// 排序完成后, ans的结果就是频次最大的k个元素
int [] ansArray = new int[k];
for (int i = 0; i < ans.size(); i ++) {
ansArray[i] = ans.get(i)[0];
}
return ansArray;
}
public void quickSort(int [][] freqArray, int start, int end, int k) {
// base case
// 注意这里是start > end
if (start > end || k == 0) {
return;
}
// 选贼第一个元素作为基准元素
int base = freqArray[start][1];
// 注意这个left要是start, 而不是start + 1
int left = start;
int right = end;
// 注意这里是小于, 不是小于等于
while (left < right) {
// 右指针先动
while (right > left && freqArray[right][1] >= base) {
right --;
}
// 注意这里freqArray[left][1] <= base 也是小于等于
while (left < right && freqArray[left][1] <= base) {
left ++;
}
// 交换left和right的元素
if (left < right) {
int [] temp = freqArray[left];
freqArray[left] = freqArray[right];
freqArray[right] = temp;
}
}
// 交换基准元素
int [] temp = freqArray[left];
freqArray[left] = freqArray[start];
freqArray[start] = temp;
// 然后计算基准元素两边的数组大小
// 结果允许按任意顺序返回前k个数字, 因此如果基准将右边分成了小于k的大小, 那么就可以记录结果了
int lenLeft = left - start;
// 右边数组包含base元素
int lenRight = end - left;
// 这里要考虑base的元素数目, 所以要 + 1
if (lenRight + 1 > k) {
// 继续对右边数组排序
quickSort(freqArray, left + 1, end, k);
} else {
// 记录右边数组的所有元素
for (int i = left; i <= end; i ++) {
ans.add(freqArray[i]);
}
// 然后对于剩下的在左边数组中排序
quickSort(freqArray, start, left - 1, k - lenRight - 1);
}
}
正确的原因
(1) 注意快排的写法
(2) 注意应该舍去哪一部分元素.
数组中的第K个最大元素
题目
版本1 快排分区
static int ans;
public static int findKthLargest(int[] nums, int k) {
// 返回第k大的数字
quickSort(nums, 0, nums.length - 1, k);
return ans;
}
public static void quickSort(int [] nums, int start, int end, int k) {
if (start > end) {
return;
}
int left = start;
int right = end;
int base = nums[start];
while (left < right) {
// 右指针先动
while (left < right && nums[right] >= base) {
right --;
}
while (left < right && nums[left] <= base) {
left ++;
}
if (left < right) {
// 交换一次
int temp = nums[right];
nums[right] = nums[left];
nums[left] = temp;
}
}
// 交换基准
nums[start] = nums[left];
nums[left] = base;
// 判断base
int lenRight = end - left;
if (lenRight == k - 1) {
ans = nums[left];
return;
}
// 如果不是刚好第k个
// 注意这里等于要去右边数组寻找
if (lenRight >= k) {
// 在右边数组继续寻找第k个
quickSort(nums, left + 1, end, k);
} else {
// 在左边数组寻找第k - lenRight - 1个
quickSort(nums, start, left - 1, k - lenRight - 1);
}
}
正确的原因
(1) 利用快排的思想对问题进行分区, 降低问题的规模.
最小的k个数字
题目
版本1 正确 快排分区
List<Integer> ans = new ArrayList<>();
public int[] getLeastNumbers(int[] arr, int k) {
// 找出最小的k个数字
// 利用快排进行分区
quickSort(arr, 0, arr.length - 1, k);
int [] ansArray = new int[k];
for (int i = 0; i < ans.size(); i ++) {
ansArray[i] = ans.get(i);
}
return ansArray;
}
public void quickSort(int [] arr, int start, int end, int k) {
if (start > end) {
return;
}
int left = start;
int right = end;
int base = arr[start];
while (left < right) {
// 右指针先动
while (left < right && arr[right] >= base) {
right --;
}
while (left < right && arr[left] <= base) {
left ++;
}
if (left < right) {
// 交换一次
int temp = arr[right];
arr[right] = arr[left];
arr[left] = temp;
}
}
// 交换基准元素
arr[start] = arr[left];
arr[left] = base;
// 开始分区
int lenLeft = left - start;
if (lenLeft + 1 > k) {
// 继续对左边的数组分区
quickSort(arr, start, left - 1, k);
} else {
// 添加结果
for (int i = start; i <= left; i ++) {
ans.add(arr[i]);
}
// 对右边的数组分区
quickSort(arr, left + 1, end, k - lenLeft - 1);
}
}
乘积小于k的数组个数
题目
版本1 滑动窗口
public static int numSubarrayProductLessThanK(int[] nums, int k) {
if (nums.length == 0 || k <= 1) {
return 0;
}
// 乘积小于k的连续子数组的个数
// 只需要统计符合要求的连续子数组的个数, 利用滑动窗口
// 当窗口内的值小于k时, 移动右指针
// 当窗口内的值大于k时, 移动左边指针, 直到窗口内的值小于k
// 记录一次结果
int left = 0;
int right = 0;
int windowMulti = 1;
int count = 0;
while (right < nums.length) {
windowMulti = windowMulti * nums[right];
while (windowMulti >= k) {
// 移动左指针
windowMulti /= nums[left];
left ++;
}
// 统计一次结果
count += right - left + 1;
right ++;
}
return count;
}
正确的原因
(1) 如果窗口内的乘积小于k, 就累加一次次数, 每次都移动右指针, 如果窗口内的乘积大于k, 一直移动左边指针.
最短无序连续子数组
题目
版本1 正确 利用额外的空间
public static int findUnsortedSubarray(int[] nums) {
// nums中有一段是无序的, 我们可以从头尾分别遍历, 很容易的得到当前无序的数组长度
// 但是此时这一段无序的数组的长度, 就算排序好了, 可能和头尾的有序数组连接不上, 因此没有用处
// 例如[1, 3, 6, 5, 2, 8, 9], 我们判断出无序数组是[5, 2], 排序完后, 整个数组为[1, 3, 6, 2, 5, 8, 9]
// 此时结果并不是有序的
// 因此我们需要得到无序数组中的最大值和最小值, 并且寻找他们在排序结束后, 应该在的索引下标
// 两个下标的差值就是我们的答案, 例如无序数组[5, 2]最小值2在最后排序数组的索引应该1, 最大值5在最后索引应该为4
// 答案就是4 - 1 + 1 = 4;
// 采用两个栈, 来得到无序数组中, 最小值和最大值, 在排序后数组中应该存在的位置
Stack<Integer> stack = new Stack<>();
// 求无序数组中最小值应该在的位置
// 求最小值, 初始值就赋予可能的最大值
int minIndex = nums.length - 1;
for (int i = 0; i < nums.length; i ++) {
// 如果当前元素比栈顶的元素小, 需要弹出元素
// 需要弹出的时候, 就代表来到来无序数组的部分
while (!stack.isEmpty() && nums[i] < nums[stack.peek()]) {
stack.pop();
// 每次弹出元素, 才表示在无序数组
// 每次弹出元素后, 此时栈顶的元素就有可能 < nums[i], 需要记录一次
// 取此时栈顶元素的时候, 需要判断栈顶是否为空
if (!stack.isEmpty()) {
minIndex = Math.min(stack.peek() + 1, minIndex);
} else {
minIndex = Math.min(0, minIndex);
}
}
// 默认添加元素
stack.push(i);
}
stack.clear();
// 求最大值, 初始值就赋予可能的最小值
int maxIndex = 0;
// 同理求无序数组最大值的索引
for (int i = nums.length - 1; i >= 0; i --) {
// 如果当前元素比栈顶的元素大, 需要弹出元素
// 需要弹出的时候, 就代表来到来无序数组的部分
while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
stack.pop();
// 每次弹出元素后, 此时栈顶的元素就有可能 > nums[i], 需要记录一次
if (!stack.isEmpty()) {
maxIndex = Math.max(stack.peek() - 1, maxIndex);
} else {
maxIndex = Math.max(nums.length - 1, maxIndex);
}
}
// 默认添加元素
stack.push(i);
}
return maxIndex - minIndex > 0 ? maxIndex - minIndex + 1 : 0;
}
正确的原因
(1) minIndex的初始值应该为nums.length - 1, maxIndex的初始值应该为0, 明确这一点, 别搞混了.
(2) 需要明确必须是在弹出元素的时候, 才需要更新minIndex的值, 并且需要考虑栈为空的情况
(3) 注意求最小值的索引的时候, 从前往后遍历, 求最大值索引的时候, 从后往前遍历
(4) 最后返回结果的时候, 如果maxIndex - minIndex < 0 就表示数组本身是有序的, 此时应该返回0
版本2 不实用额外的空间, 最优的解法
public static int findUnsortedSubarray(int[] nums) {
// nums中有一段是无序的, 我们可以从头尾分别遍历, 很容易的得到当前无序的数组长度
// 但是此时这一段无序的数组的长度, 就算排序好了, 可能和头尾的有序数组连接不上, 因此没有用处
// 例如[1, 3, 6, 5, 2, 8, 9], 我们判断出无序数组是[5, 2], 排序完后, 整个数组为[1, 3, 6, 2, 5, 8, 9]
// 此时结果并不是有序的
// 因此我们需要得到无序数组中的最大值和最小值, 并且寻找他们在排序结束后, 应该在的索引下标
// 两个下标的差值就是我们的答案, 例如无序数组[5, 2]最小值2在最后排序数组的索引应该1, 最大值5在最后索引应该为4
// 答案就是4 - 1 + 1 = 4;
// 寻找到无序数组中的最小值和最大值
// 然后通过遍历数组的方式, 寻找到最小值在排序数组中的位置
int min = Integer.MAX_VALUE;
// 遍历一遍数组, 寻找无序数组的最小值
// flag表示, 是否进入到无序数组的范围中
boolean flag = Boolean.FALSE;
for (int i = 0; i < nums.length; i ++) {
if (i > 0 && nums[i] < nums[i - 1]) {
flag = Boolean.TRUE;
}
// 此时nums[i]是无序数组的一部分了
if (flag) {
min = Math.min(min, nums[i]);
}
}
// 同理求最大值
int max = Integer.MIN_VALUE;
flag = Boolean.FALSE;
for (int i = nums.length - 1; i >= 0; i --) {
if (i < nums.length - 1 && nums[i] > nums[i + 1]) {
flag = Boolean.TRUE;
}
// 此时nums[i]是无序数组的一部分了
if (flag) {
max = Math.max(max, nums[i]);
}
}
// 寻找min在数组中正确的索引位置
int minIndex = 0;
for (int i = 0; i < nums.length; i ++) {
if (nums[i] <= min) {
minIndex ++;
} else {
break;
}
}
// 寻找max在数组中正确的索引
int maxIndex = nums.length - 1;
for (int i = nums.length - 1; i >= 0; i --) {
if (nums[i] >= max) {
maxIndex --;
} else {
break;
}
}
return maxIndex - minIndex > 0 ? maxIndex - minIndex + 1 : 0;
}
正确的原因
(1) 在求解无序数组中最大最小值的时候, 需要判断是否进入了无序数组的范围, 才进行更新
(2) 因为数组中可能存在相同的值, 例如下图, 因此在求最小值, 最大值应该的排序位置时, 应该是大于等于, 而不是单纯的大于
if (nums[i] >= max) {
maxIndex --;
} else {
break;
}
搜索旋转排序数组
题目
版本1 正确
public int search(int[] nums, int target) {
// 在旋转排序数组中, 寻找目标元素
// [1, 2, 3, 4, 5, 6]旋转完后变成[4, 5, 6, 1, 2, 3]
// 利用二分查找加快搜寻的速度
// 对于旋转后的数组, 选择任意一个位置将数组分割开来, 一定有一半是有序的
// 就可以根据有序的那一半来帮助判断, 目标元素应该在哪一半中
// 如何判断哪一半是有序数组呢?
// 数组旋转后, 左边数组的最小值, 一定是右边数组的最大值
// 因此如果每次采用mid对于数组进行分割的话, 就可以比较nums[mid]和nums[0]和nums[nums.length - 1]这三个值的大小
// 注意, 这里比的一定是nums[0], nums[mid]和nums[nums.length - 1], 而不是和二分查找的左右边界的值比较
// 如果nums[mid] >= nums[0], 那么left到mid这一段一定是有序数组
// 如果nums[mid] <= nums[nums.length - 1], 那么mid到right这一段一定是有序数组
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] >= nums[0]) {
// left到mid是有序数组
// 注意判断target是否在有序数组需要两个条件, 1. 要大于有序数组的最小值 2. 小于有序数组最大值
if (nums[left] <= target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else if (nums[mid] <= nums[nums.length - 1]) {
// mid到right是有序数组
// 同理
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
正确的原因
(1) 注意如何判断mid分割开的两个数组, 哪个是有序的, 哪个是无序的
(2) 对于有序数组, 需要判断target是否在有序数组的范围内, 不能只判断一边, 忽略另一边, 即需要nums[left] <= target && target < nums[mid]而不是单单判断target < nums[mid]