31. 下一个排列
下一个排列
问题描述
这道题是 LeetCode 31题。
“下一个排列”的定义是:给定数字序列的字典序中下一个更大的排列。如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
我们可以将该问题形式化地描述为:给定若干个数字,将其组合为一个整数。如何将这些数字重新排列,以得到下一个更大的整数。如 123 下一个更大的数为 132。如果没有更大的整数,则输出最小的整数。
以 1,2,3,4,5,6 为例,其排列依次为:
123456
123465
123546
…
654321
可以看到有这样的关系:123456 < 123465 < 123546 < … < 654321。
算法推导
如何得到这样的排列顺序?这是本文的重点。我们可以这样来分析:
我们希望下一个数比当前数大,这样才满足“下一个排列”的定义。因此只需要将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
我们还希望下一个数增加的幅度尽可能的小,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:
在尽可能靠右的低位进行交换,需要从后向前查找
将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
将「大数」换到前面后,需要将「大数」后面的所有数重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列
以上就是求“下一个排列”的分析过程。
算法过程
标准的“下一个排列”算法可以描述为:
从后向前查找第一个相邻升序的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序
在 [j,end) 从后向前查找第一个满足 A[i] < A[k] 的 k。A[i]、A[k] 分别就是上文所说的「小数」、「大数」
将 A[i] 与 A[k] 交换
可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序
如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4
该方法支持数据重复,且在 C++ STL 中被采用。
class Solution {
public void nextPermutation(int[] nums) {
int n = 0;
if(nums == null || (n = nums.length) == 0) return;
// 从后往前查找第一次出现 nums[i] < nums[i+1] 的位置
int i = n-2;
for(; i>=0 && nums[i] >= nums[i+1]; i--) ;
// if i == -1 nums 则整体逆序
if(i >= 0){
// 此时 nums[i+1, n-1] 降序, 查找其中比 大于nums[i] 的 最小的值,可以直接 从后往前 逆向找
// 因为有序 可以练习一下二分查找
int j = binarySearch(nums, i+1, n-1, nums[i]);
// 交换 i j
swap(nums, i, j);
}
// 此时 nums[i+1, n-1] 仍然降序,将其改为升序,只需要反转即可。
reverse(nums, i+1, n-1);
}
// nums[left, right] 逆序,查找其中 > target 的 最大下标
private int binarySearch(int[] nums, int left, int right, int target){
while(left <= right){
int mid = (left + right) >>> 1;
if(nums[mid] > target){
left = mid + 1; // 尝试再缩小区间
}else{
right = mid - 1;
}
}
return right;
}
private void swap(int[] nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
private void reverse(int[] nums, int i, int j){
while(i < j){
swap(nums, i++, j--);
}
}
}
摘自
可视化
以求 12385764 的下一个排列为例:
首先从后向前查找第一个相邻升序的元素对 (i,j)。这里 i=4,j=5,对应的值为 5,7:
然后在 [j,end) 从后向前查找第一个大于 A[i] 的值 A[k]。这里 A[i] 是 5,故 A[k] 是 6:
将 A[i] 与 A[k] 交换。这里交换 5、6:
这时 [j,end) 必然是降序,逆置 [j,end),使其升序。这里逆置 [7,5,4]:
因此,12385764 的下一个排列就是 12386457。
最后再可视化地对比一下这两个相邻的排列(橙色是蓝色的下一个排列):
162.递归迭代二分寻找峰值
算法题:给定一个数组,求峰值(比左边一个大,比右边一个大),暴力O(n)很显然,但是有logn的。我想到了一部分,如果两边都不是峰值,那么峰值一定在中间。这是可以二分的。但是我没敢说,因为太紧张了我无法证明。归根结底是我对二分理解得不够深入,之前遇到的二分都是排好序的数组的二分,这次一个没排序的也可以二分让我大开眼界。本题凉
普通线性查找:
#include<bits/stdc++.h>
using namespace std;
/***********************************递归二分查找*******************************************/
int helper(int *a,int l,int r){
if(l == r) return l;
int mid = (l + r)/2;
if(a[mid] > a[mid+1]) return helper(a,l,mid);
return helper(a,mid+1,r);
}
int peak1(int *a, int len){
return helper(a,0,len-1);
}
/***********************************递归二分查找*******************************************/
/***********************************迭代二分查找*******************************************/
int peak2(int *a, int l, int r){
while(l < r){
int mid = (l+r)/2;
if(a[mid]> a[mid+1]) r = mid;
else l = mid+1;
}
return r;
}
/***********************************迭代二分查找*******************************************/
int main(){
int a[]= {1};
int len = sizeof(a)/sizeof(a[0]);
cout << peak1(a, len)<<endl;
cout << peak2(a, 0, len-1)<<endl;
}
40.最小的k个数
快速排序:
参考链接:https://blog.csdn.net/nrsc272420199/article/details/82587933
package com.nrsc.sort;
public class QuickSort {
public static void main(String[] args) {
int[] arr = { 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
quickSort(arr, 0, arr.length - 1);
System.out.println("排序后:");
for (int i : arr) {
System.out.println(i);
}
}
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找寻基准数据的正确索引
int index = getIndex(arr, low, high);
// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
quickSort(arr, 0, index - 1);
quickSort(arr, index + 1, high);
}
}
private static int getIndex(int[] arr, int low, int high) {
// 基准数据
int tmp = arr[low];
while (low < high) {
// 当队尾的元素大于等于基准数据时,向前挪动high指针
while (low < high && arr[high] >= tmp) {
high--;
}
// 如果队尾元素小于tmp了,需要将其赋值给low
arr[low] = arr[high];
// 当队首元素小于等于tmp时,向前挪动low指针
while (low < high && arr[low] <= tmp) {
low++;
}
// 当队首元素大于tmp时,需要将其赋值给high
arr[high] = arr[low];
}
// 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
// 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
arr[low] = tmp;
return low; // 返回tmp的正确位置
}
}
堆排序
https://www.cnblogs.com/chengxiao/p/6129630.html
a.将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
- 复杂度O(nlogk):topk问题:
- 现在有n个数,设计算法得到前k大的数。(k<n)
- 先 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数(最小的数)。
- 再 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行依次调整。
- 最后 遍历列表所有元素后,倒序弹出堆顶。
- 升序用大顶堆,降序用小顶堆
#include<iostream>
using namespace std;
void print(int arr[],int len){
for(int i=0;i<len;i++){
cout<<arr[i]<<" ";
}
cout<<endl;
}
void swap(int arr[],int i,int j){
int temp=arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
void adjustHeap(int arr[],int i,int len){
int temp=arr[i]; //先取出当前元素i
for(int k=2*i+1;k<len;k=2*k+1){ //从i结点的左子结点开始,也就是2i+1处开始
if(k+1<len && arr[k]<arr[k+1]){ //如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k]>temp){ //如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i=k;
}else{
break;
}
}
arr[i] = temp; //将temp值放到最终的位置
}
void sort(int arr[],int len){
//1.构建大顶堆
for(int i=len/2-1;i>=0;i--){ //由下至上从最后一个非叶子节点开始创建堆
adjustHeap(arr,i,len);
print(arr,len);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=len-1;j>=0;j--){
swap(arr,0,j); //将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j); //重新对堆进行调整
print(arr,len);
}
}
int main(){
int arr[10] = {9,8,7,6,5,4,3,2,1,0};
int len = sizeof(arr)/sizeof(arr[0]);
sort(arr,len);
return 0;
}
快速排序(递归)
复杂度为O(nlogn)
逆序最坏复杂度变成了O(n^2)
不稳定,是冒泡排序的跳跃
分治核心,归并排序也是分治核心
package com.nrsc.sort;
public class QuickSort {
public static void main(String[] args) {
int[] arr = { 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
quickSort(arr, 0, arr.length - 1);
System.out.println("排序后:");
for (int i : arr) {
System.out.println(i);
}
}
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找寻基准数据的正确索引
int index = getIndex(arr, low, high);
// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
quickSort(arr, 0, index - 1);
quickSort(arr, index + 1, high);
}
}
private static int getIndex(int[] arr, int low, int high) {
// 基准数据
int tmp = arr[low];
while (low < high) {
// 当队尾的元素大于等于基准数据时,向前挪动high指针
while (low < high && arr[high] >= tmp) {
high--;
}
// 如果队尾元素小于tmp了,需要将其赋值给low
arr[low] = arr[high];
// 当队首元素小于等于tmp时,向前挪动low指针
while (low < high && arr[low] <= tmp) {
low++;
}
// 当队首元素大于tmp时,需要将其赋值给high
arr[high] = arr[low];
}
// 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
// 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
arr[low] = tmp;
return low; // 返回tmp的正确位置
}
}
归并排序
稳定
时间复杂度 O(nlogn)
#include<iostream>
#include<vector>
using namespace std;
void print(int arr[],int len){
for(int i=0;i<len;i++){
cout<<arr[i]<<" ";
}
cout<<endl;
}
void merge(int arr[],int l,int mid,int r){
int *help = new int(r-l+1);
int p=l,q=mid+1;
int i=0;
while(p<=mid && q<=r){
help[i++] = arr[p]<arr[q]?arr[p++]:arr[q++];
}
while(p<=mid) help[i++] = arr[p++];
while(q<=r) help[i++] = arr[q++];
for(int j=0;j<r-l+1;j++){
arr[l+j] = help[j];
}
}
void MergeSort(int arr[],int l,int r){
if(l<r){
int mid = (l+r)/2;
MergeSort(arr,l,mid);
MergeSort(arr,mid+1,r);
merge(arr,l,mid,r);
}
}
int main(){
int arr[10] = {9,8,7,6,5,4,3,2,1,0};
int len = sizeof(arr)/sizeof(arr[0]);
MergeSort(arr,0,len-1);
print(arr,len);
return 0;
}