[LeetCode] Minimum Moves to Equal Array Elements II 最少移动次数使数组元素相等之二
Given a non-empty integer array, find the minimum number of moves required to make all array elements equal, where a move is incrementing a selected element by 1 or decrementing a selected element by 1.
You may assume the array's length is at most 10,000.
Example:
Input: [1,2,3] Output: 2 Explanation: Only two moves are needed (remember each move increments or decrements one element): [1,2,3] => [2,2,3] => [2,2,2]
这道题是之前那道Minimum Moves to Equal Array Elements的拓展,现在我们可以每次对任意一个数字加1或者减1,让我们用最少的次数让数组所有值相等。一般来说这种题目是不能用暴力方法算出所有情况,因为OJ一般是不会答应的。那么这道题是否像上面一道题一样,有巧妙的方法呢?答案是肯定的。下面这种解法实际上利用了之前一道题Best Meeting Point的思想,是不感觉很amazing,看似完全不相干的两道题,居然有着某种内部联系。我们首先给数组排序,那么我们最终需要变成的相等的数字就是中间的数,如果数组有奇数个,那么就是最中间的那个数字;如果是偶数个,那么就是中间两个数的区间中的任意一个数字。而两端的数字变成中间的一个数字需要的步数实际上就是两端数字的距离,讲到这里发现是不是就和这道题Best Meeting Point的思路是一样了。那么我们就两对两对的累加它们的差值就可以了,参见代码如下:
解法一:
class Solution { public: int minMoves2(vector<int>& nums) { int res = 0, i = 0, j = (int)nums.size() - 1; sort(nums.begin(), nums.end()); while (i < j) { res += nums[j--] - nums[i++]; } return res; } };
既然有了上面的分析,我们知道实际上最后相等的数字就是数组的最中间的那个数字,那么我们在给数组排序后,直接利用坐标定位到中间的数字,然后算数组中每个数组与其的差的绝对值累加即可,参见代码如下:
解法二:
class Solution { public: int minMoves2(vector<int>& nums) { sort(nums.begin(), nums.end()); int res = 0, mid = nums[nums.size() / 2]; for (int num : nums) { res += abs(num - mid); } return res; } };
上面的两种方法都给整个数组排序了,时间复杂度是O(nlgn),其实我们并不需要给所有的数字排序,我们只关系最中间的数字,那么这个stl中自带的函数nth_element就可以完美的发挥其作用了,我们只要给出我们想要数字的位置,它就能在O(n)的时间内返回正确的数字,然后算数组中每个数组与其的差的绝对值累加即可,参见代码如下:
解法三:
class Solution { public: int minMoves2(vector<int>& nums) { int res = 0, n = nums.size(), mid = n / 2; nth_element(nums.begin(), nums.begin() + mid, nums.end()); for (int i = 0; i < n; ++i) { res += abs(nums[i] - nums[mid]); } return res; } };
下面这种方法是改进版的暴力破解法,它遍历了所有的数字,让每个数字都当作最后相等的值,然后算法出来总步数,每次和res比较,留下较小的。而这种方法叼就叼在它在O(1)的时间内完成了步数统计,那么这样整个遍历下来也只是O(n)的时间,不过由于还是要给数组排序,所以整体的时间复杂度是O(nlgn),这已经能保证可以通过OJ啦。那么我们来看看如何快速计算总步数,首先我们给数组排序,我们假设中间某个位置有个数字k,那么此时数组就是:nums[0], nums[1], ..., k, ..., nums[n - 1], 如果i为数字k在数组中的坐标,那么有k = nums[i],那么总步数为:
Y = k - nums[0] + k - nums[1] + ... + k - nums[i - 1] + nums[i] - k + nums[i + 1] - k + ... + nums[n - 1] - k
= i * k - (nums[0] + nums[1] + ... + nums[i - 1]) + (nums[i] + nums[i + 1] + ... + nums[n - 1]) - (n - i) * k
= 2 * i * k - n * k + sum - 2 * curSum
那么我们只要算出sum和curSum就可以快速得到总步数了,数组之和可以通过遍历数组计算出来,curSum可以在遍历的过程中累加,那么我们就可以算出总步数,然后每次更新结果res了,参见代码如下:
解法四:
class Solution { public: int minMoves2(vector<int>& nums) { sort(nums.begin(), nums.end()); long long sum = accumulate(nums.begin(), nums.end(), 0); long long res = LONG_MAX, curSum = 0; int n = nums.size(); for (int i = 0; i < n; ++i) { long long k = nums[i]; curSum += k; res = min(res, 2 * k * (i + 1) - n * k + sum - 2 * curSum); } return res; } };
类似题目:
Minimum Moves to Equal Array Elements
参考资料:
https://discuss.leetcode.com/topic/68764/5-line-solution-with-comment
https://discuss.leetcode.com/topic/68884/c-average-o-n-nth_element-solution
https://discuss.leetcode.com/topic/68736/java-just-like-meeting-point-problem
https://discuss.leetcode.com/topic/68900/simple-c-sort-and-find-solution-with-detailed-explanation