归位算法

380 阅读3分钟

涉及的题目有数组中重复的数字丢失的数字找到所有数组中消失的数字缺少的第一个正数寻找重复数

这类题目基本上要求常数级别的空间,时间复杂度也只有O(N)。题目解题关键是利用数组本身是一个hash表,然后让数组上的数字回归到对应的下标的位置。

归位算上是指通过swap(arr[index], arr[arr[index]]),将值arr[index]归位到[arr[index]]这个下标位置处。将元素都归位后,就会出现元素的值就等于其下标。

在实操归位算法时,一般是两重循环。外层循环是遍历数组的所有元素。内层循环则是对index位置不断做swap操作。 之所以内层循环是这样是因为swap操作之后,不是将index值归位到index位置,而是将当前[index]位置上的值归位到arr[index]。可以说swap是将[index]位置上的值归位到本属于它的位置上,而不是为位置[index]找到了值index。 一次swap操作后,[index]位置上的值,可能还是不等于index。此时还得继续swap,将这个新的值再归位。因此是一个循环操作。 什么时候终止内层循环? 一般是swap之后index上的元素值等于index,或者题目的其他限制条件。如果碰到限制条件直接中断内层循环即可,这个中断并不会阻碍元素无法归位。后面的题目会有挺多这类的限制,做题时需要一一找出限制条件。

对于归位题目,需要先思考:假如将所有元素能归位都归位后,得到的情景会是怎么样的? 一般来说, 归位后,数组中所有元素的值都等于其下标。 此时结合题目的条件,那些剩下不能归位或者值不配位的元素往往就是题目所求。

注意:那些重复、越界和小于0的数字,何处安放并不重要,重要的是尽量将能归位的元素都归位。

数组中重复的数字

原题链接。题目如下:

找出数组中重复的数字。

在一个长度为 n 的数组nums里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3 

有数字重复,那么在归位时会有至少两个元素抢一个位置。当发现这种抢的情景出现就说明此时的元素是重复的。

AC代码如下:

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        for(int i = 0; i < nums.size(); ++i)//对每一个位置上的元素都尝试归位
        {
            // != 说明还i位置上的元素还没得到归位,需要不断循环直至归位。
            while(nums[i] != i)
            {
                if(nums[i] == nums[nums[i]])//nums[nums[i]]这个地方已经被占用了。那么说明重复了
                    return nums[i];

                //将nums[i]这个值归位到下标为[nums[i]]的位置上。 
                std::swap(nums[i], nums[nums[i]]);
            }
        }

        return 0;//should not reach
    }
};

这道题目主要是用来熟悉归位算法的代码结构。

丢失的数字

原题链接,描述如下:

给定一个包含 [0, n]n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

示例 1:
输入:nums = [3,0,1]
输出:2
解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。

示例 2:
输入:nums = [0,1]
输出:2
解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。

示例 3:
输入:nums = [9,6,4,2,3,5,7,0,1]
输出:8
解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。

不管那么多,先归位。如果没有丢失数字,那么数组上所有元素的值都等于其下标。 此时扫描一遍归位后的数组,看看那个下标的值不等于下标,那么就是所求了。对于例1,归位后得到[0, 1, 3]。扫描一遍数组,发现下标2对应的元素值是3,值不配位,因此2就是所求了。

这道题目比前面一道的复杂之处在于需要考虑下面两种情况:1. 重复数字; 2. 下标会越界。这两种情况都会终止该下标的归位。

对于第一种情况,在归位的时候会出现死循环。比如数组中有两个元素的值为3,第一个3已经归位到下标为3的位置了。此时在归位下标为5的值,并且当前arr[5]等于3。那么应该终止对于下标为5位置的归位。因为经过swap(arr[5], arr[arr[5]])(也就是swap[arr[5], arr[3])之后,arr[5]的值还是3,陷入死循环。 为此,碰到重复元素的需要终止归位。

对于第二种情况,下标越界。比如例1中的第一个元素3,明显是越界的。不能将3归位。

AC代码如下:

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        for(int i = 0; i < nums.size(); ++i)
        {
            while(nums[i] != i)
            {
                //何时终止swap?
                //这道题目需要考虑有重复的数字、值等于nums.size()的数字。
                //重复的值也就是要判定nums[i]这个下标上的值是否为nums[i]。如果
                //等于,那么其实是不能替换的,因为当i这个下标上的值也为nums[i]的时候
                //会出现死循环。因为更换之后,没有任何变化。
                if(nums[i] >= nums.size() || nums[nums[i]] == nums[i])
                    break;

                std::swap(nums[i], nums[nums[i]]);
            }
        }

        int i = 0;
        while(i < nums.size() && nums[i] == i )
            ++i;

        return i; 
    }
};

找到所有数组中消失的数字

原题链接。题目描述:

给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。

找到所有在 [1, n] 范围之间没有出现在数组中的数字。

您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。

示例:

输入:
[4,3,2,7,8,2,3,1]

输出:
[5,6]

不管那么多,先归位。归位后,如果没有消失的数字,那么数组里面,所有元素的值都等于其下标。 如果某个数字消失了,那么对应下标位置上的值就不会等于该下标。因此找的也是值不配位。

AC的解答和前面的差不多

class Solution {
public:
    vector<int> findDisappearedNumbers(vector<int>& nums) {
        std::vector<int> ret;

        //先减一,以免后面混乱。此时所有元素值都在[0, nums.size())之间
        std::transform(nums.begin(), nums.end(), nums.begin(), std::bind1st(std::plus<int>(), -1));

        for(int i = 0; i < nums.size(); ++i)
        {
            while(nums[i] != i && nums[i] != nums[nums[i]])
            {
                std::swap(nums[i], nums[nums[i]]);
            }
        }

        for(int i = 0; i < nums.size(); ++i)
        {
            if(i != nums[i])
                ret.push_back(i+1);//加回1
        }

        return ret;
    }
};

缺失的第一个正数

原题链接,题目描述:

给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。

例子:

示例 1:
输入: [1,2,0]
输出: 3

示例 2:
输入: [3,4,-1,1]
输出: 2

示例 3:
输入: [7,8,9,11,12]
输出: 1

直接归位,那些越界的元素安放何处不重要。 归位后,扫描一遍数组,碰到第一个值不配位的就是所求了。

AC代码如下:

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        nums.push_back(0);//保证0这个下标有值。因为0并不是题目所求。

        for(int i = 0; i < nums.size(); ++i)
        {
            while(nums[i] != i)
            {
                //终止条件需要考虑全面。
                if(nums[i] < 0 || nums[i] >= nums.size() || nums[i] == nums[nums[i]])
                    break;

                std::swap(nums[i], nums[nums[i]]);
            }
        }

        int i = 0;
        while(i < nums.size() && nums[i] == i )
            ++i;

        return i;
    }
};

第一时间接收最新文章,请关注同名微信公众号: 代码的色彩