简化理解:先从n-2的位置向后找,找到一个当前数比前一个数小的位置,否则就后移,只要这个位置是合法的,那么再从n-1的您方找到一个当前数比刚才定位的数大的位置,同样,否则就后移。定位到两个位置之后进行交换,交换完成之后进行一个翻转,翻转位置为(i+1,n-1)。
过程
// 1 2 5 8 7
//ans
// 1 2 7 5 8
//process
// 1 2 7 8 5
// 1 2 7 5 8
代码:
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
int i = n - 2;
while(i >= 0 && nums[i] >= nums[i + 1]){
i--;
}
if(i >= 0){
int j = n - 1;
while(j > i && nums[j] <= nums[i]){
j--;
}
swap(nums, i, j);
}
reverse(nums, i + 1, n - 1);
}
public void reverse(int[] nums, int low, int high){
while(low < high){
swap(nums, low++, high--);
}
}
public void swap(int[] nums, int low, int high){
int temp = nums[low];
nums[low] = nums[high];
nums[high] = temp;
}
}
核心思想,最后返回的因为是正整数,那么这个数一定是[1,n+1]。
执行过程:
[3,4,-1,1]=>[3,4,5,1]=>[-3,4,-5,-1]
代码:
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for(int i = 0; i < n; i++){
if(nums[i] <= 0){
nums[i] = n + 1;
}
}
for(int i = 0; i < n; i++){
int num = Math.abs(nums[i]);
if(num <= n){
nums[num - 1] = -Math.abs(nums[num - 1]);
}
}
for(int i = 0; i < n; i++){
if(nums[i] > 0){
return i + 1;
}
}
return n + 1;
}
}
推荐题解
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
//先沿斜对角线翻转
for(int i = 0;i < n;i ++)
for(int j = 0;j < i;j ++){
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
//再沿垂直竖线翻转,要使用另外一个变量从倒数第一个进行翻转
for(int i = 0;i < n;i ++)
for(int j = 0, k = n - 1; j < k ; j++, k--){
int temp = matrix[i][k];
matrix[i][k] = matrix[i][j];
matrix[i][j] = temp;
}
}
}
模拟过程
假设pivot为3,以下标pivot为基准,比nums[3]=5大的在左边,比其小的在右边。
把5放在最左边,即:交换low和pivot下标的元素。
此时慢指针i为low,i指向每次找到的比5大的数
快指针j为low+1,j去寻找比nums[low]大的数,也就是base。
进循环:
找到num[j]比5大的数,交换nums[i+1]和nums[j],并且i++
不管有没有找到比5大的数,j都要++
此时结果是[5,6,1,3,2,4]
再交换low和i小标的元素,因为当前i所在的位置就是数组中比基准大的最后一个位置;交换完成之后,i位置之前的都是比它大的,i位置之后都是比它小的。
交换后[6,5,1,3,2,4]
但是并不是每次都这么凑巧,所以要分治
如果当前i就是第k个元素,即:i == k-1,找到topK,返回nums[i]
如果当前i > k - 1,记住i下标的元素就是base元素,这个时候表示的就是前面有很多比base元素大的,那么就进行区间治理,在[low,i-1]之间找
如果当前i < k - 1,这个时候表示后面又很多元素比base小的,同样要在[i+1,high]找。因为i指向的是base
代码
class Solution {
public int findKthLargest(int[] nums, int k) {
return findTopKth(0, nums.length - 1, nums, k);
}
private int findTopKth(int low, int high, int[] nums, int k){
int pivot = low + (int)(Math.random() * (high - low + 1));
swap(nums,low,pivot);
int base = nums[low];
int i = low, j = low + 1;
while(j <= high){
if(nums[j] > base){
swap(nums, i + 1, j);
i++;
}
j++;
}
swap(nums, low, i);
if(i == k - 1){
return nums[i];
}else if(i > k - 1){
return findTopKth(low, i - 1, nums, k);
}else {
return findTopKth(i + 1, high, nums, k);
}
}
private void swap(int[] nums, int a, int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
res[i] * k
,同时k每次更新为nums[i] * k
。简化理解:使用一个变量k(初值为1)和一个结果数组res,首先res中记录k变化值,k进行变化(该过程从前往后)。然后k重新设置1,res保存的就是结果,k也进行变化(该过程是从后往前)。k值的变化过程都是nums[i] * k
执行过程,以[1,2,3,4]
为例子
执行完第一循环之后,res为[1,1,2,6]
这个就是除去数组当前元素的左边元素乘积
重新初始化k之后
res[3] = res[3] * k // k就是这个数右边的乘积
k = k * nums[3] // 更新k为4,因为k代表该数右边的乘积,那么更新之后就是概述右边的乘积 * 该数左边的乘积
代码
class Solution {
public int[] productExceptSelf(int[] nums) {
int[] res = new int[nums.length];
int k = 1;
for(int i = 0; i < res.length; i++){
res[i] = k;
k = k * nums[i]; // 此时数组存储的是除去当前元素左边的元素乘积
}
k = 1;
for(int i = res.length - 1; i >= 0; i--){
res[i] *= k; // k为该数右边的乘积。
k *= nums[i]; // 此时数组等于左边的 * 该数右边的。
}
return res;
}
}
题解:【动画模拟】一下就能读懂(单调双端队列) - 滑动窗口最大值 - 力扣(LeetCode) (leetcode-cn.com)
思路:
- 维护一个
单调递减
的双端队列。- 递归循环
指定窗口大小次数
,只要当前队列不为空,那么队尾的元素比当前元素小
,那么移除
队尾的元素,这样,就可以找到第一个元素
,将该元素即对头元素,送至结果数组idx=1的位置。- 继续移动窗口,判断
当前窗口前的元素是否和对头元素相等
,如果相等就出队。这一步就是保证窗口大小
,也确保了维护的双端队列大小最多为k。- 继续按照之前的规则进行入队,依旧是维护单调递减的队列。
- 每次将对头元素存放在数组里面。
- 返回数组。
执行过程,以示例1为例
nums:[1,3,-1,-3,5,3,6,7], k = 3
开始创建一个双端队列
[1]->[3]->[3,-1]
然后将此时队列的对头元素加入到结果数组中
然后从nums[k]开始扫描,每次首先要判断当前窗口元素是否和对头元素相等,相等就出队。
进行扫描
[3,-1]->[3,-1,-3]
再扫描的时候,发现当前窗口的元素和对头元素相等(3 == 3),移除对头元素
[3,-1,-3]->[-1,-3]
继续扫描,此时5都比对中元素大,从队尾移除
[-1,-3]->[5]
[5]->[5,3]->[6]->[7]
其实解释保持双端队列的单调递减,以及保证双端队列的大小最大为k,也就是为什么要进行当前窗口元素和对头元素是否相等。此时移除从对头移除。
当前元素值比队尾元素值小,移除从队尾进行移除
每次想数组添加元素是添加对头元素。
代码
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length;
if(len == 0){
return nums;
}
int[] res = new int[len - k + 1];
int idx = 0;
Deque<Integer> deque = new LinkedList<>();
for(int i = 0; i < k; i++){
while(!deque.isEmpty() && deque.peekLast() < nums[i]){
deque.removeLast();
}
deque.offerLast(nums[i]);
}
res[idx++] = deque.peekFirst();
for(int j = k; j < len; j++){
//因为是从k开始的,所以在每次判断时候要减去k
if(nums[j - k] == deque.peekFirst()){
deque.removeFirst();
}
while(!deque.isEmpty() && deque.peekLast() < nums[j]){
deque.removeLast();
}
deque.offerLast(nums[j]);
res[idx++] = deque.peekFirst();
}
return res;
}
}
推荐题解
- 遍历数组,将数存放在双向队列中,并用 L,R 来标记窗口的左边界和右边界。队列中保存的是该数值对应的数组下标位置,并且数组中的数要
从大到小排序
。- 如果
当前遍历的数比队尾的值大
,则需要弹出
队尾值,直到队列重新满足从大到小的要求
。(保证一定从大到小,后面的数字一定要比前面的大)- 刚开始遍历时,L 和 R 都为 0,有一个形成窗口的过程,此过程没有最大值,L 不动,R 向右移。
- 当窗口大小形成时,L 和 R 一起向右移,每次移动时,判断
队首的值的数组下标是否在 [L,R] 中
,如果不在则需要弹出队首的值,当前窗口的最大值即为队首的数。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length < 2) return nums;
// 双向队列 保存当前窗口最大值的数组位置 保证队列中数组位置的数值按从大到小排序
LinkedList<Integer> queue = new LinkedList();
// 结果数组
int[] result = new int[nums.length-k+1];
// 遍历nums数组
for(int i = 0;i < nums.length;i++){
// 保证从大到小 如果前面数小则需要依次弹出,直至满足要求
while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
queue.pollLast();
}
// 添加当前值对应的数组下标
queue.addLast(i);
// 判断存储的下标是否在队列中
if(queue.peek() <= i-k){
queue.poll();
}
// 当窗口长度为k时 保存当前窗口中最大值
//从现在开始往数组里添加答案了,因为 i 至少已经走了一个窗口的大小
if(i+1 >= k){
result[i+1-k] = nums[queue.peek()];
}
}
return result;
}
}
- 先对每个区间的左端点进行一个升序排序。
- 然后依次获取每个区间的左右端点,并使用一个列表进行一个存储。
- 当首次添加,即:列表为空,或者前一个右端点比当前的左端点的值小,那么添加当前的区间。
- 否则,即:前一个的右端点比当前的左端点大
代码:
class Solution {
public int[][] merge(int[][] intervals) {
if(intervals.length == 0){
return new int[0][2];
}
//按每个区间的左端点升序排列。
Arrays.sort(intervals, new Comparator<int[]>() {
public int compare(int[] v1, int[] v2){
return v1[0] - v2[0];
}
});
List<int[]> res = new ArrayList<>();
for(int i = 0; i < intervals.length; i++){
int L = intervals[i][0], R = intervals[i][1];
//结果列表为空,就添加第一个,或者,前一个数组的右侧小于现在的左侧
if(res.size() == 0 || res.get(res.size() - 1)[1] < L){
res.add(new int[]{L, R});
}else {
//否则说明列表不为空,并且前一个数组的右侧大于或者等于现在的左侧,
//那么进行更新,原则是将前一个数组的右侧更新为现在右侧和前一个数组右侧的最大值
res.get(res.size() - 1)[1] = Math.max(res.get(res.size() - 1)[1], R);
}
}
return res.toArray(new int[res.size()][]);
}
}
思路:
代码:
class Solution {
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList();
int len = nums.length;
if(nums == null || len < 3) return ans;
Arrays.sort(nums); // 排序
for (int i = 0; i < len ; i++) {
//先固定住一个数,然后再进行其他两个数的寻找
if(nums[i] > 0) break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
//其他的两个数分别在固定住数的左右找
int L = i+1;
int R = len-1;
while(L < R){
int sum = nums[i] + nums[L] + nums[R];
if(sum == 0){
//添加之后不管去不去重,双指针都要移动
ans.add(Arrays.asList(nums[i],nums[L],nums[R]));
while (L<R && nums[L] == nums[L+1]) L++; // 去重
while (L<R && nums[R] == nums[R-1]) R--; // 去重
L++;
R--;
}
else if (sum < 0) L++;
else if (sum > 0) R--;
}
}
return ans;
}
}
代码:
class Solution {
public void moveZeroes(int[] nums) {
int n = nums.length, left = 0, right = 0;
while(right < n){
if(nums[right] != 0){
swap(nums, left, right);
left++;
}
right++;
}
}
public void swap(int[] nums, int left, int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
思路:
代码:
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int[] res = new int[m + n];
int i = 0, j = 0;
int temp = 0;
while(i < m || j < n){
if(i == m){
temp = nums2[j++];
}else if(j == n){
temp = nums1[i++];
}else if(nums1[i] <= nums2[j]){
temp = nums1[i++];
}else if(nums1[i] > nums2[j]){
temp = nums2[j++];
}
res[i + j - 1] = temp;
}
int count = 0;
while(count != m + n){
nums1[count] = res[count];
count++;
}
}
}
思路:
int[][] dirs = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
分别表示向右,向下,向左,向上。这个顺序也正好符合了顺时针螺旋式的添加。int nx = x + dirs[d][0] ,ny = y + dirs[d][1];
因为第一次是向右的,且d开始为0,那么最开始的时候就是向右走。代码:
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<>();
int row = matrix.length, col = matrix[0].length;
int[][] dirs = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
for(int x = 0, y = 0, d = 0, i = 0; i< row * col; i++){
res.add(matrix[x][y]);
matrix[x][y] = Integer.MAX_VALUE;
int nx = x + dirs[d][0] ,ny = y + dirs[d][1];
if(nx < 0 || nx >= row || ny < 0 || ny >= col || matrix[nx][ny] == Integer.MAX_VALUE){
d = (d + 1) % 4;
nx = x + dirs[d][0];
ny = y + dirs[d][1];
}
x = nx;
y = ny;
}
return res;
}
}
重点是记住上下左右四个方向怎么描述。
思路:
代码
版本1:原生
原生的思想:
使用两个指针low,high,在大前提low小于high的情况下,进行交换,pivot的值选择为nums[low],在循环的时候high去找比pivot小的值,没找到就--,找到就停,然后交换low和high的值,low去找比pivot大的值,找到就停,没找到就++。然后交换low和high的值,最后low停的位置pivot值,最后再递归即可。在(low,mid - 1)和(mid + 1, high)中进行。
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 mid= partition(nums, low, high);
quickSort(nums, low, mid - 1);
quickSort(nums, mid + 1, high);
}
}
public int partition(int[] nums, int low, int high){
int pivot = nums[low];
while(low < high){
while(low < high && nums[high] >= pivot){
high--;
}
if(low < high){
nums[low] = nums[high];
}
while(low < high && nums[low] <= pivot){
low++;
}
if(low < high){
nums[high] = nums[low];
}
}
nums[low] = pivot;
return low;
}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
版本2:改进
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 mid = partition(nums, low, high);
quickSort(nums, low, mid - 1);
quickSort(nums, mid + 1, high);
}
}
private int partition(int[] nums, int low, int high){
int pivot = low + (int)(Math.random() * (high - low + 1));
swap(nums, low, pivot);
int i = low, j = low + 1;
int base = nums[low];
while(j <= high){
if(nums[j] < base){
i++;
swap(nums, i, j);
}
j++;
}
swap(nums, i, low);
return i;
}
private void swap(int[] nums, int a, int b){
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
模拟过程同215一致,只是每次是先交换的小的,而215每次交换的是大的。
思路二:
思考:为什么在215的时候,交换是(i + 1, j),但是本题却是(++i, j),有何不同?
归并排序:
归并过程:
重点在mergeTwo
代码,好好背背。
归并代码:
class Solution {
public int[] sortArray(int[] nums) {
mergeSort(nums, 0, nums.length - 1);
return nums;
}
private void mergeSort(int[] nums, int low, int high){
if(low < high){
int mid = low + (high - low) / 2;
mergeSort(nums, low, mid);
mergeSort(nums, mid + 1, high);
mergeTwoArrays(nums, low, mid, high);
}
}
private void mergeTwoArrays(int[] nums, int low, int mid, int high){
int[] tmp = new int[high - low + 1];
int i = low, j = mid + 1, k = 0;
while(i <= mid && j <= high){
if(nums[i] < nums[j]){
tmp[k++] = nums[i++];
}else{
tmp[k++] = nums[j++];
}
}
while(i <= mid) tmp[k++] = nums[i++];
while(j <= high) tmp[k++] = nums[j++];
for(int index = 0; index < k; index++){
nums[low + index] = tmp[index];
}
}
}
堆排序(大顶堆):建议嗯背
堆排序代码:
public class Sort{
public int[] HeapSort(int[] nums){
//构建大根堆
buildMaxHeap(nums);
for(int i = nums.length - 1; i > 0; i--){
//将大根堆的堆顶和最后一个叶子结点交换,最大值在尾部。
swap(nums, 0, i);
//对前i个元素构建新的大顶堆
heapify(nums, 0, i);
}
}
//构建大顶堆(从第一个非叶子结点从右到左,从下到上)
public void buildMaxheap(int[] nums){
for(int i = nums.length / 2 - 1; i >= 0; i--){
heapify(nums, i, nums.length);
}
}
public void heapify(int[] nums, int i, int length){
int left = 2 * i + 1;
int right = 2 * i + 2;
//默认最大值是当前i根节点
int largest = i;
//左子结点存在并且大于根节点
if(left < length && nums[left] > nums[largest]){
largest = left;
}
//右子结点存在并且小于根节点
if(right < length && nums[right] < nums[largest]){
largest = right;
}
//最大值是子结点
if(largest != i){
swap(nums, largest, i);
//交换完毕之后再调整其子结点为根的堆
heapify(nums, largest, length);
}
}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> res = new LinkedList<>();
int n = nums.length;
for(int i = 0; i < n; i++){
if(nums[i] <= 0){
nums[i] = n + 1;
}
}
for(int i = 0; i < n; i++){
int num = Math.abs(nums[i]);
if(num <= n){
nums[num - 1] = -Math.abs(nums[num - 1]);
}
}
for(int i = 0; i < n; i++){
if(nums[i] > 0){
res.add(i + 1);
}
}
return res;
}
}
因为要求在不使用额外空间且时间复杂度为O(n)
情况下去解决,那么就不能使用暴力解法。那么就要考虑下标与数字的关系。(标记法)
例如:
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
nums | 4 | 3 | 2 | 7 | 8 | 2 | 3 | 1 |
可以看出缺少[5,6]
如果是正常排列的话,那么就是
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
nums | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
所以:首先对数组进行扫描,先得到每次数字对应的下标,即:
index | 3 | 2 | 1 | 6 | 7 | 1 | 2 | 0 |
---|---|---|---|---|---|---|---|---|
num | 4 | 3 | 2 | 7 | 8 | 2 | 3 | 1 |
然后再将对应位置的数变为其对应的负数
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
nums | -4 | -3 | -2 | -7 | 8 | 2 | -3 | -1 |
开始的时候[4....8]
将数组对应的元素修改成了负数,之后[2,3]
数字先是得到下标,然后再获取到数组中的数,对其进行abs,再在前面加一个符号,这样就能保证出现的数永远都是负的。
代码
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> res = new ArrayList<>();
for(int i = 0; i < nums.length; i++){
//获取每个数字应该放入的下标
int idx = Math.abs(nums[i]) - 1;
//将对应位置的数字变为负数
nums[idx] = -Math.abs(nums[idx]);
}
for(int i = 0; i < nums.length; i++){
if(nums[i] > 0){
res.add(i + 1);
}
}
return res;
}
}
public int longestConsecutive(int[] nums) {
int n = nums.length;
if(n == 0){
return 0;
}
Arrays.sort(nums);
int maxLen = 1;
int len = 1;
for(int i = 1; i < n; i++){
if(nums[i] == nums[i - 1]){
//相等的
continue;
}else if(nums[i] == nums[i - 1] + 1){
//正好大1
len++;
}else {
maxLen = Math.max(maxLen, len);
len = 1;
}
}
return Math.max(maxLen, len);
}