最小可用Id的命令式解法

劉新宇發表於2017-02-04

在《演算法新解》的前言中,我們只給出了使用分而治之策略的線性時間O(n),常數空間O(1)的解法。這裡我們給出另外兩種命令式解法,它們都可以達到同樣的效能。

首先我們先回顧一下書中討論的一個重要性質

1 <= answer <= n + 1

其中n是序列的長度,當原始序列是1到n的某個排列時,答案為n+1。

鴿籠排序[1]

任何基於比較的排序都會將時間效能降低到O(n lg n),所以諸如堆,排序二叉樹這類的解法無法滿足要求。我們可以將第i個元素x放置到它“應該”在的位置上:也就是第x個位置。如果x比n大,根據上面的公式,我們知道它一定不是答案,因此可以跳過它不做處理。否則,我們將第i個元素和第x個元素交換。由於我們交換到第i個位置上的元素不一定等於i,因此需要不斷重複交換直到第i個元素比n大或者等於i。我們從1到n對i進行迭代,處理所有元素。由於每個元素僅被交換一次,因此這一處理是線性時間的(你能嚴格證明這一結論麼?)。

接下來,我們再次掃描一遍這一序列,如果在任何位置i上,元素值不等於i,我們就找到了最終的答案。否則,說明原始序列是“滿”的,我們返回n+1作為最小可用元素。

下面是一個C風格的C++例子程式碼。

int findMinFree1(vector<int> nums) {
    int i, n = nums.size();
    for (i = 0; i < n; ++i)
        while (nums[i] <= n && nums[i] != i + 1)
            swap(nums[i], nums[nums[i] - 1]);
    for (i = 0; i < n; ++i)
        if (i + 1 != nums[i])
            return i + 1;
    return n + 1;
}

符號編碼

不論是使用一個標記陣列,還是將每個元素放置到“正確”的位置上,我們本質上是想知道一個元素是否存在。存在與否是一種二值化(binary)資訊。我們可以將其編碼為數字的正負號。思路是如果元素x存在於序列中,我們就將第x個元素從正數反轉為負數。當我們把所有不大於n的元素的正負號都反轉完,我們可以再從左向右掃描一遍序列,找到第一個正數,它所在的位置就是最終的答案。

下面是這一方法的C風格的C++例子程式碼:

int findMinFree2(vector<int> nums) {
    int i, k, n = nums.size();
    for (i = 0; i < n; ++i)
        if ((k = abs(nums[i])) <= n)
            nums[k - 1] = - abs(nums[k - 1]);
    for (i = 0; i < n; ++i)
        if (nums[i] > 0)
            return i + 1;
    return n + 1;
}

[1] https://en.wikipedia.org/wiki/Pigeonhole_sort

相關文章