1. 程式人生 > 其它 >LeetCode 31. 下一個排列(C++) 雙指標

LeetCode 31. 下一個排列(C++) 雙指標

技術標籤:LeetCode刷題資料結構演算法leetcode陣列指標

在這裡插入圖片描述
示例 1:

輸入:nums = [1,2,3]
輸出:[1,3,2]

示例 2:

輸入:nums = [3,2,1]
輸出:[1,2,3]

示例 3:

輸入:nums = [1,1,5]
輸出:[1,5,1]

示例 4:

輸入:nums = [1]
輸出:[1]

在這裡插入圖片描述

前言

本題要求我們實現一個演算法,將給定數字序列重新排列成字典序中下一個更大的排列。
以數字序列 [1,2,3] 為例,其排列按照字典序依次為:
[1,2,3]
[1,3,2]
[2,1,3]
[2,3,1]
[3,1,2]
[3,2,1]

這樣,排列 [2,3,1] 的下一個排列即為 [3,1,2]。特別的,最大的排列[3,2,1] 的下一個排列為最小的排列 [1,2,3]。

兩遍掃描思路及解法

注意到下一個排列總是比當前排列要大,除非該排列已經是最大的排列。我們希望找到一種方法,能夠找到一個大於當前序列的新序列,且變大的幅度儘可能小。具體地:
我們需要將一個左邊的「較小數」與一個右邊的「較大數」交換,以能夠讓當前排列變大,從而得到下一個排列。
同時我們要讓這個「較小數」儘量靠右,而「較大數」儘可能小。當交換完成後,「較大數」右邊的數需要按照升序重新排列。這樣可以在保證新排列大於原來排列的情況下,使變大的幅度儘可能小。
以排列 [4,5,2,6,3,1]為例:
我們能找到的符合條件的一對「較小數」與「較大數」的組合為 2 與 3,滿足「較小數」儘量靠右,而「較大數」儘可能小。

當我們完成交換後排列變為 [4,5,3,6,2,1],此時我們可以重排「較小數」右邊的序列,序列變為 [4,5,3,1,2,6]。
具體地,我們這樣描述該演算法,對於長度為 n 的排列 a:

首先從後向前查詢第一個順序對(i,i+1),滿足 a[i]<a[i+1]。這樣「較小數」即為 a[i]。此時 [i+1,n) 必然是下降序列。
如果找到了順序對,那麼在區間 [i+1,n) 中從後向前查詢第一個元素 j 滿足 a[i]<a[j]。這樣「較大數」即為 a[j]。
交換 a[i] 與 a[j],此時可以證明區間 [i+1,n) 必為降序。我們可以直接使用雙指標反轉區間 [i+1,n) 使其變為升序,而無需對該區間進行排序。

注意
如果在步驟 1 找不到順序對,說明當前序列已經是一個降序序列,即最大的序列,我們直接跳過步驟 2 執行步驟 3,即可得到最小的升序序列。
該方法支援序列中存在重複元素,且在 C++ 的標準庫函式 next_permutation 中被採用。

class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        int i = nums.size() - 2;//長度減1 即是陣列的倒數第二個元素
        while (i >= 0 && nums[i] >= nums[i + 1]) {//nums[i] >= nums[i + 1]表示這兩個元素是降序
        //直到nums[i] < nums[i + 1] 兩個元素升序
            i--;
        }
        if (i >= 0) {
            int j = nums.size() - 1;//j為最後一個元素
            while (j >= 0 && nums[i] >= nums[j]) {//直到nums[i] < nums[j]跳出迴圈
                j--;
            }
            swap(nums[i], nums[j]);//交換較小值與較大值的位置
        }
        reverse(nums.begin() + i + 1, nums.end());//第i+2個到最後一個 翻轉順序
    }
};

複雜度分析

時間複雜度:O(N),其中 N 為給定序列的長度。我們至多隻需要掃描兩次序列,以及進行一次反轉操作。
空間複雜度:O(1),只需要常數的空間存放若干變數。