经典算法题(二)——解Integer Array - 整型数组

本章主要总结与整型数组相关的题。

 

1.Remove Element

Question

  • leetcode: Remove Element | LeetCode OJ
  • lintcode: (172) Remove Element
Given an array and a value, remove all occurrences of that value in place and return the new length.

The order of elements can be changed, and the elements after the new length don't matter.

Example
Given an array [0,4,4,0,0,2,4,4], value=4

return 4 and front four elements of the array is [0,0,0,2]

 

题解1 - 使用容器

入门题,返回删除指定元素后的数组长度,使用容器操作非常简单。以 lintcode 上给出的参数为例,遍历容器内元素,若元素值与给定删除值相等,删除当前元素并往后继续遍历。

C++

class Solution {
public:
    /**
     *@param A: A list of integers
     *@param elem: An integer
     *@return: The new length after remove
     */
    int removeElement(vector &A, int elem) {
        for (vector::iterator iter = A.begin(); iter < A.end(); ++iter) {
            if (*iter == elem) {
                iter = A.erase(iter);
                --iter;
            }
        }

        return A.size();
    }
};

源码分析

注意在遍历容器内元素和指定欲删除值相等时,需要先自减--iter, 因为for循环会对iter自增,A.erase()删除当前元素值并返回指向下一个元素的指针,一增一减正好平衡。如果改用while循环,则需注意访问数组时是否越界。

复杂度分析

由于vector每次erase的复杂度是$$O(n)$$,我们遍历整个数组,最坏情况下,每个元素都与要删除的目标元素相等,每次都要删除元素的复杂度高达$$O(n^2)$$ 观察此方法会如此低效的原因,是因为我们一次只删除一个元素,导致很多没必要的元素交换移动,如果能够将要删除的元素集中处理,则可以大幅增加效率,见题解2。

题解2 - 两根指针

由于题中明确暗示元素的顺序可变,且新长度后的元素不用理会。我们可以使用两根指针分别往前往后遍历,头指针用于指示当前遍历的元素位置,尾指针则用于在当前元素与欲删除值相等时替换当前元素,两根指针相遇时返回尾指针索引——即删除元素后「新数组」的长度。

C++

class Solution {
public:
    int removeElement(int A[], int n, int elem) {
        for (int i = 0; i < n; ++i) {
            if (A[i] == elem) {
                A[i] = A[n - 1];
                --i;
                --n;
            }
        }

        return n;
    }
};

源码分析

遍历当前数组,A[i] == elem时将数组「尾部(以 n 为长度时的尾部)」元素赋给当前遍历的元素。同时自减in,原因见题解1的分析。需要注意的是n在遍历过程中可能会变化。

复杂度分析

此方法只遍历一次数组,且每个循环的操作至多也不过仅是常数次,因此时间复杂度是$$O(n)$$

Reference

  • Remove Element | 九章算法

 

2.Zero Sum Subarray

Question

  • lintcode: (138) Subarray Sum
  • GeeksforGeeks: Find if there is a subarray with 0 sum - GeeksforGeeks
Given an integer array, find a subarray where the sum of numbers is zero.
Your code should return the index of the first number and the index of the last number.

Example
Given [-3, 1, 2, -3, 4], return [0, 2] or [1, 3].

Note
There is at least one subarray that it's sum equals to zero.

 

题解1 - 两重 for 循环

题目中仅要求返回一个子串(连续)中和为0的索引,而不必返回所有可能满足题意的解。最简单的想法是遍历所有子串,判断其和是否为0,使用两重循环即可搞定,最坏情况下时间复杂度为 $$O(n^2)$$, 这种方法显然是极其低效的,极有可能会出现 TLE. 下面就不浪费篇幅贴代码了。

题解2 - 比较子串和(TLE)

两重 for 循环显然是我们不希望看到的解法,那么我们再来分析下题意,题目中的对象是分析子串和,那么我们先从常见的对数组求和出发,$$f(i) = \sum _{0} ^{i} nums[i]$$ 表示从数组下标 0 开始至下标 i 的和。子串和为0,也就意味着存在不同的 $$i_1$$$$i_2$$ 使得 $$f(i_1) - f(i_2) = 0$$, 等价于 $$f(i_1) = f(i_2)$$. 思路很快就明晰了,使用一 vector 保存数组中从 0 开始到索引i的和,在将值 push 进 vector 之前先检查 vector 中是否已经存在,若存在则将相应索引加入最终结果并返回。

C++

class Solution {
public:
    /**
     * @param nums: A list of integers
     * @return: A list of integers includes the index of the first number
     *          and the index of the last number
     */
    vector subarraySum(vector nums){
        vector result;

        int curr_sum = 0;
        vector sum_i;
        for (int i = 0; i != nums.size(); ++i) {
            curr_sum += nums[i];

            if (0 == curr_sum) {
                result.push_back(0);
                result.push_back(i);
                return result;
            }

            vector::iterator iter = find(sum_i.begin(), sum_i.end(), curr_sum);
            if (iter != sum_i.end()) {
                result.push_back(iter - sum_i.begin() + 1);
                result.push_back(i);
                return result;
            }

            sum_i.push_back(curr_sum);
        }

        return result;
    }
};

源码分析

使用curr_sum保存到索引i处的累加和,sum_i保存不同索引处的和。执行sum_i.push_back之前先检查curr_sum是否为0,再检查curr_sum是否已经存在于sum_i中。是不是觉得这种方法会比题解1好?错!时间复杂度是一样一样的!根本原因在于find操作的时间复杂度为线性。与这种方法类似的有哈希表实现,哈希表的查找在理想情况下可认为是 $$O(1)$$.

复杂度分析

最坏情况下 $$O(n^2)$$, 实测和题解1中的方法运行时间几乎一致。

题解3 - 哈希表

终于到了祭出万能方法时候了,题解2可以认为是哈希表的雏形,而哈希表利用空间换时间的思路争取到了宝贵的时间资源 :)

C++

class Solution {
public:
    /**
     * @param nums: A list of integers
     * @return: A list of integers includes the index of the first number
     *          and the index of the last number
     */
    vector subarraySum(vector nums){
        vector result;
        // curr_sum for the first item, index for the second item
        map hash;
        hash[0] = 0;

        int curr_sum = 0;
        for (int i = 0; i != nums.size(); ++i) {
            curr_sum += nums[i];
            if (hash.find(curr_sum) != hash.end()) {
                result.push_back(hash[curr_sum]);
                result.push_back(i);
                return result;
            } else {
                hash[curr_sum] = i + 1;
            }
        }

        return result;
    }
};

源码分析

为了将curr_sum == 0的情况也考虑在内,初始化哈希表后即赋予 <0, 0>. 给 hash赋值时使用i + 1, push_back时则不必再加1.

由于 C++ 中的map采用红黑树实现,故其并非真正的「哈希表」,C++ 11中引入的unordered_map用作哈希表效率更高,实测可由1300ms 降至1000ms.

复杂度分析

遍历求和时间复杂度为 $$O(n)$$, 哈希表检查键值时间复杂度为 $$O(\log L)$$, 其中 $$L$$ 为哈希表长度。如果采用unordered_map实现,最坏情况下查找的时间复杂度为线性,最好为常数级别。

题解4 - 排序

除了使用哈希表,我们还可使用排序的方法找到两个子串和相等的情况。这种方法的时间复杂度主要集中在排序方法的实现。由于除了记录子串和之外还需记录索引,故引入pair记录索引,最后排序时先按照sum值来排序,然后再按照索引值排序。如果需要自定义排序规则可参考[^sort_pair_second].

C++

class Solution {
public:
    /**
     * @param nums: A list of integers
     * @return: A list of integers includes the index of the first number
     *          and the index of the last number
     */
    vector subarraySum(vector nums){
        vector result;
        if (nums.empty()) {
            return result;
        }

        const int num_size = nums.size();
        vector > sum_index(num_size + 1);
        for (int i = 0; i != num_size; ++i) {
            sum_index[i + 1].first = sum_index[i].first + nums[i];
            sum_index[i + 1].second = i + 1;
        }

        sort(sum_index.begin(), sum_index.end());
        for (int i = 1; i < num_size + 1; ++i) {
            if (sum_index[i].first == sum_index[i - 1].first) {
                result.push_back(sum_index[i - 1].second);
                result.push_back(sum_index[i].second - 1);
                return result;
            }
        }

        return result;
    }
};

源码分析

没啥好分析的,注意好边界条件即可。这里采用了链表中常用的「dummy」节点方法,pair排序后即为我们需要的排序结果。这种排序的方法需要先求得所有子串和然后再排序,最后还需要遍历排序后的数组,效率自然是比不上哈希表。但是在某些情况下这种方法有一定优势。

复杂度分析

遍历求子串和,时间复杂度为 $$O(n)$$, 空间复杂度 $$O(n)$$. 排序时间复杂度近似 $$O(n \log n)$$, 遍历一次最坏情况下时间复杂度为 $$O(n)$$. 总的时间复杂度可近似为 $$O(n \log n)$$. 空间复杂度 $$O(n)$$.

扩展

这道题的要求是找到一个即可,但是要找出所有满足要求的解呢?Stackoverflow 上有这道延伸题的讨论[^stackoverflow].

另一道扩展题来自 Google 的面试题 - Find subarray with given sum - GeeksforGeeks.

Reference

  • [^stackoverflow]: algorithm - Zero sum SubArray - Stack Overflow
  • [^sort_pair_second]: c++ - How do I sort a vector of pairs based on the second element of the pair? - Stack Overflow

 

3.Subarray Sum K

Question

  • GeeksforGeeks: Find subarray with given sum - GeeksforGeeks
Given an nonnegative integer array, find a subarray where the sum of numbers is k.
Your code should return the index of the first number and the index of the last number.

Example
Given [1, 4, 20, 3, 10, 5], sum k = 33, return [2, 4].

 

题解1 - 哈希表

题 Zero Sum Subarray | Data Structure and Algorithm 的升级版,这道题求子串和为 K 的索引。首先我们可以考虑使用时间复杂度相对较低的哈希表解决。前一道题的核心约束条件为 $$f(i_1) - f(i_2) = 0$$,这道题则变为 $$f(i_1) - f(i_2) = k$$

C++

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    /**
     * @param nums: A list of integers
     * @return: A list of integers includes the index of the first number
     *          and the index of the last number
     */
    vector subarraySum(vector nums, int k){
        vector result;
        // curr_sum for the first item, index for the second item
        // unordered_map hash;
        map hash;
        hash[0] = 0;

        int curr_sum = 0;
        for (int i = 0; i != nums.size(); ++i) {
            curr_sum += nums[i];
            if (hash.find(curr_sum - k) != hash.end()) {
                result.push_back(hash[curr_sum - k]);
                result.push_back(i);
                return result;
            } else {
                hash[curr_sum] = i + 1;
            }
        }

        return result;
    }
};

int main(int argc, char *argv[])
{
    int int_array1[] = {1, 4, 20, 3, 10, 5};
    int int_array2[] = {1, 4, 0, 0, 3, 10, 5};
    vector vec_array1;
    vector vec_array2;
    for (int i = 0; i != sizeof(int_array1) / sizeof(int); ++i) {
        vec_array1.push_back(int_array1[i]);
    }
    for (int i = 0; i != sizeof(int_array2) / sizeof(int); ++i) {
        vec_array2.push_back(int_array2[i]);
    }

    Solution solution;
    vector result1 = solution.subarraySum(vec_array1, 33);
    vector result2 = solution.subarraySum(vec_array2, 7);

    cout << "result1 = [" << result1[0] << " ," << result1[1] << "]" << endl;
    cout << "result2 = [" << result2[0] << " ," << result2[1] << "]" << endl;

    return 0;
}

源码分析

与 Zero Sum Subarray 题的变化之处有两个地方,第一个是判断是否存在哈希表中时需要使用hash.find(curr_sum - k), 最终返回结果使用result.push_back(hash[curr_sum - k]);而不是result.push_back(hash[curr_sum]);

复杂度分析

略,见 Zero Sum Subarray | Data Structure and Algorithm

题解2 - 利用单调函数特性

不知道细心的你是否发现这道题的隐含条件——nonnegative integer array, 这也就意味着子串和函数 $$f(i)$$ 为「单调不减」函数。单调函数在数学中可是重点研究的对象,那么如何将这种单调性引入本题中呢?不妨设 $$i_2 > i_1$$, 题中的解等价于寻找 $$f(i_2) - f(i_1) = k$$, 则必有 $$f(i_2) \geq k$$.

我们首先来举个实际例子帮助分析,以整数数组 {1, 4, 20, 3, 10, 5} 为例,要求子串和为33的索引值。首先我们可以构建如下表所示的子串和 $$f(i)$$.

$$f(i)$$ 1 5 25 28 38
$$i$$ 0 1 2 3 4

要使部分子串和为33,则要求的第二个索引值必大于等于4,如果索引值再继续往后遍历,则所得的子串和必大于等于38,进而可以推断出索引0一定不是解。那现在怎么办咧?当然是把它扔掉啊!第一个索引值往后递推,直至小于33时又往后递推第二个索引值,于是乎这种技巧又可以认为是「两根指针」。

C++

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    /**
     * @param nums: A list of integers
     * @return: A list of integers includes the index of the first number
     *          and the index of the last number
     */
    vector subarraySum2(vector &nums, int k){
        vector result;

        int left_index = 0, curr_sum = 0;
        for (int i = 0; i != nums.size(); ++i) {
            while (curr_sum > k) {
                curr_sum -= nums[left_index];
                ++left_index;
            }

            if (curr_sum == k) {
                result.push_back(left_index);
                result.push_back(i - 1);
                return result;
            }
            curr_sum += nums[i];
        }
        return result;
    }
};

int main(int argc, char *argv[])
{
    int int_array1[] = {1, 4, 20, 3, 10, 5};
    int int_array2[] = {1, 4, 0, 0, 3, 10, 5};
    vector vec_array1;
    vector vec_array2;
    for (int i = 0; i != sizeof(int_array1) / sizeof(int); ++i) {
        vec_array1.push_back(int_array1[i]);
    }
    for (int i = 0; i != sizeof(int_array2) / sizeof(int); ++i) {
        vec_array2.push_back(int_array2[i]);
    }

    Solution solution;
    vector result1 = solution.subarraySum2(vec_array1, 33);
    vector result2 = solution.subarraySum2(vec_array2, 7);

    cout << "result1 = [" << result1[0] << " ," << result1[1] << "]" << endl;
    cout << "result2 = [" << result2[0] << " ," << result2[1] << "]" << endl;

    return 0;
}

源码分析

使用for循环, 在curr_sum > k时使用while递减curr_sum, 同时递增左边索引left_index, 最后累加curr_sum。如果顺序不对就会出现 bug, 原因在于判断子串和是否满足条件时在递增之后(谢谢 @glbrtchen 汇报 bug)。

复杂度分析

看似有两重循环,由于仅遍历一次数组,且索引最多挪动和数组等长的次数。故最终时间复杂度近似为 $$O(2n)$$, 空间复杂度为 $$O(1)$$.

Reference

  • Find subarray with given sum - GeeksforGeeks

 

4.Subarray Sum Closest

Question

  • lintcode: (139) Subarray Sum Closest
Given an integer array, find a subarray with sum closest to zero.
Return the indexes of the first number and last number.

Example
Given [-3, 1, 1, -3, 5], return [0, 2], [1, 3], [1, 1], [2, 2] or [0, 4]

Challenge
O(nlogn) time

 

 

题解

题 Zero Sum Subarray | Data Structure and Algorithm 的变形题,由于要求的子串和不一定,故哈希表的方法不再适用,使用解法4 - 排序即可在 $$O(n \log n)$$ 内解决。具体步骤如下:

  1. 首先遍历一次数组求得子串和。
  2. 对子串和排序。
  3. 逐个比较相邻两项差值的绝对值,返回差值绝对值最小的两项。

C++

class Solution {
public:
    /**
     * @param nums: A list of integers
     * @return: A list of integers includes the index of the first number
     *          and the index of the last number
     */
    vector subarraySumClosest(vector nums){
        vector result;
        if (nums.empty()) {
            return result;
        }

        const int num_size = nums.size();
        vector > sum_index(num_size + 1);

        for (int i = 0; i < num_size; ++i) {
            sum_index[i + 1].first = sum_index[i].first + nums[i];
            sum_index[i + 1].second = i + 1;
        }

        sort(sum_index.begin(), sum_index.end());

        int min_diff = INT_MAX;
        int closest_index = 1;
        for (int i = 1; i < num_size + 1; ++i) {
            int sum_diff = abs(sum_index[i].first - sum_index[i - 1].first);
            if (min_diff > sum_diff) {
                min_diff = sum_diff;
                closest_index = i;
            }
        }

        int left_index = min(sum_index[closest_index - 1].second,\
                             sum_index[closest_index].second);
        int right_index = -1 + max(sum_index[closest_index - 1].second,\
                                   sum_index[closest_index].second);
        result.push_back(left_index);
        result.push_back(right_index);
        return result;
    }
};

源码分析

为避免对单个子串和是否为最小情形的单独考虑,我们可以采取类似链表 dummy 节点的方法规避,简化代码实现。故初始化sum_index时需要num_size + 1个。这里为避免 vector 反复扩充空间降低运行效率,使用resize一步到位。sum_index即最后结果中left_indexright_index等边界可以结合简单例子分析确定。

复杂度分析

  1. 遍历一次求得子串和时间复杂度为 $$O(n)$$, 空间复杂度为 $$O(n+1)$$.
  2. 对子串和排序,平均时间复杂度为 $$O(n \log n)$$.
  3. 遍历排序后的子串和数组,时间复杂度为 $$O(n)$$.

总的时间复杂度为 $$O(n \log n)$$, 空间复杂度为 $$O(n)$$.

扩展

  • algorithm - How to find the subarray that has sum closest to zero or a certain value t in O(nlogn) - Stack Overflow

 

5.Recover Rotated Sorted Array

Question

  • lintcode: (39) Recover Rotated Sorted Array
Given a rotated sorted array, recover it to sorted array in-place.

Example
[4, 5, 1, 2, 3] -> [1, 2, 3, 4, 5]

Challenge
In-place, O(1) extra space and O(n) time.

Clarification
What is rotated array:

    - For example, the orginal array is [1,2,3,4], The rotated array of it can be [1,2,3,4], [2,3,4,1], [3,4,1,2], [4,1,2,3]

首先可以想到逐步移位,但是这种方法显然太浪费时间,不可取。下面介绍利器『三步翻转法』,以[4, 5, 1, 2, 3]为例。

  1. 首先找到分割点51
  2. 翻转前半部分4, 55, 4,后半部分1, 2, 3翻转为3, 2, 1。整个数组目前变为[5, 4, 3, 2, 1]
  3. 最后整体翻转即可得[1, 2, 3, 4, 5]

由以上3个步骤可知其核心为『翻转』的in-place实现。使用两个指针,一个指头,一个指尾,使用for循环移位交换即可。

Java

public class Solution {
    /**
     * @param nums: The rotated sorted array
     * @return: The recovered sorted array
     */
    public void recoverRotatedSortedArray(ArrayList nums) {
        if (nums == null || nums.size() <= 1) {
            return;
        }

        int pos = 1;
        while (pos < nums.size()) { // find the break point
            if (nums.get(pos - 1) > nums.get(pos)) {
                break;
            }
            pos++;
        }
        myRotate(nums, 0, pos - 1);
        myRotate(nums, pos, nums.size() - 1);
        myRotate(nums, 0, nums.size() - 1);
    }

    private void myRotate(ArrayList nums, int left, int right) { // in-place rotate
        while (left < right) {
            int temp = nums.get(left);
            nums.set(left, nums.get(right));
            nums.set(right, temp);
            left++;
            right--;
        }
    }
}

C++

/**
 * forked from
 * http://www.jiuzhang.com/solutions/recover-rotated-sorted-array/
 */
class Solution {
private:
    void reverse(vector &nums, vector::size_type start, vector::size_type end) {
        for (vector::size_type i = start, j = end; i < j; ++i, --j) {
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
        }
    }

public:
    void recoverRotatedSortedArray(vector &nums) {
        for (vector::size_type index = 0; index != nums.size() - 1; ++index) {
            if (nums[index] > nums[index + 1]) {
                reverse(nums, 0, index);
                reverse(nums, index + 1, nums.size() - 1);
                reverse(nums, 0, nums.size() - 1);

                return;
            }
        }
    }
};

源码分析

首先找到分割点,随后分三步调用翻转函数。简单起见可将vector::size_type替换为int

 

6.Product of Array Exclude Itself

Question

  • lintcode: (50) Product of Array Exclude Itself
  • GeeksforGeeks: A Product Array Puzzle - GeeksforGeeks
Given an integers array A.

Define B[i] = A[0] * ... * A[i-1] * A[i+1] * ... * A[n-1], calculate B WITHOUT divide operation.

Example
For A=[1, 2, 3], return [6, 3, 2].

 

题解1 - 左右分治

根据题意,有 $$result[i] = left[i] \cdot right[i]$$, 其中 $$left[i] = \prod _{j = 0} ^{i - 1} A[j]$$, $$right[i] = \prod _{j = i + 1} ^{n - 1} A[j]$$. 即将最后的乘积分为两部分求解,首先求得左半部分的值,然后求得右半部分的值。最后将左右两半部分乘起来即为解。

C++

class Solution {
public:
    /**
     * @param A: Given an integers array A
     * @return: A long long array B and B[i]= A[0] * ... * A[i-1] * A[i+1] * ... * A[n-1]
     */
    vector productExcludeItself(vector &nums) {
        const int nums_size = nums.size();
        vector result(nums_size, 1);
        if (nums.empty() || nums_size == 1) {
            return result;
        }

        vector left(nums_size, 1);
        vector right(nums_size, 1);
        for (int i = 1; i != nums_size; ++i) {
            left[i] = left[i - 1] * nums[i - 1];
            right[nums_size - i - 1] = right[nums_size - i] * nums[nums_size - i];
        }
        for (int i = 0; i != nums_size; ++i) {
            result[i] = left[i] * right[i];
        }

        return result;
    }
};

源码分析

一次for循环求出左右部分的连乘积,下标的确定可使用简单例子辅助分析。

复杂度分析

两次for循环,时间复杂度 $$O(n)$$. 使用了左右两半部分辅助空间,空间复杂度 $$O(2n)$$.

题解2 - 原地求积

题解1中使用了左右两个辅助数组,但是仔细瞅瞅其实可以发现完全可以在最终返回结果result基础上原地计算左右两半部分的积。

C++

class Solution {
public:
    /**
     * @param A: Given an integers array A
     * @return: A long long array B and B[i]= A[0] * ... * A[i-1] * A[i+1] * ... * A[n-1]
     */
    vector productExcludeItself(vector &nums) {
        const int nums_size = nums.size();
        vector result(nums_size, 1);

        // solve the left part first
        for (int i = 1; i < nums_size; ++i) {
            result[i] = result[i - 1] * nums[i - 1];
        }

        // solve the right part
        long long temp = 1;
        for (int i = nums_size - 1; i >= 0; --i) {
            result[i] *= temp;
            temp *= nums[i];
        }

        return result;
    }
};

源码分析

计算左半部分的递推式不用改,计算右半部分的乘积时由于会有左半部分值的干扰,故使用temp保存连乘的值。注意temp需要使用long long, 否则会溢出。

复杂度分析

时间复杂度同上,空间复杂度为 $$O(1)$$.

 

7.Partition Array

Question

  • lintcode: (31) Partition Array

Problem Statement

Given an array nums of integers and an int k, partition the array (i.e move the elements in "nums") such that:

  • All elements < k are moved to the left
  • All elements >= k are moved to the right

Return the partitioning index, i.e the first index i nums[i] >= k.

Example

If nums = [3,2,2,1] and k=2, a valid answer is 1.

Note

You should do really partition in array nums instead of just counting the numbers of integers smaller than k.

If all elements in nums are smaller than k, then return nums.length

Challenge

Can you partition the array in-place and in O(n)?

 

题解1 - 自左向右

容易想到的一个办法是自左向右遍历,使用right保存大于等于 k 的索引,i则为当前遍历元素的索引,总是保持i >= right, 那么最后返回的right即为所求。

C++

class Solution {
public:
    int partitionArray(vector &nums, int k) {
        int right = 0;
        const int size = nums.size();
        for (int i = 0; i < size; ++i) {
            if (nums[i] < k && i >= right) {
                int temp = nums[i];
                nums[i] = nums[right];
                nums[right] = temp;
                ++right;
            }
        }

        return right;
    }
};

源码分析

自左向右遍历,遇到小于 k 的元素时即和right索引处元素交换,并自增right指向下一个元素,这样就能保证right之前的元素一定小于 k. 注意if判断条件中i >= right不能是i > right, 否则需要对特殊情况如全小于 k 时的考虑,而且即使考虑了这一特殊情况也可能存在其他 bug. 具体是什么 bug 呢?欢迎提出你的分析意见~

复杂度分析

遍历一次数组,时间复杂度最少为 $$O(n)$$, 可能需要一定次数的交换。

题解2 - 两根指针

有了解过 Quick Sort 的做这道题自然是分分钟的事,使用左右两根指针 $$left, right$$ 分别代表小于、大于等于 k 的索引,左右同时开工,直至 $$left > right$$.

C++

class Solution {
public:
    int partitionArray(vector &nums, int k) {
        int left = 0, right = nums.size() - 1;
        while (left <= right) {
            while (left <= right && nums[left] < k) ++left;
            while (left <= right && nums[right] >= k) --right;
            if (left <= right) {
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                ++left;
                --right;
            }
        }

        return left;
    }
};

源码分析

大循环能正常进行的条件为 $$left <= right$$, 对于左边索引,向右搜索直到找到小于 k 的索引为止;对于右边索引,则向左搜索直到找到大于等于 k 的索引为止。注意在使用while循环时务必进行越界检查!

找到不满足条件的索引时即交换其值,并递增left, 递减right. 紧接着进行下一次循环。最后返回left即可,当nums为空时包含在left = 0之中,不必单独特殊考虑,所以应返回left而不是right.

复杂度分析

只需要对整个数组遍历一次,时间复杂度为 $$O(n)$$, 相比题解1,题解2对全小于 k 的数组效率较高,元素交换次数较少。

Reference

  • Partition Array | 九章算法

 

8.First Missing Positive

Question

  • leetcode: First Missing Positive | LeetCode OJ
  • lintcode: (189) First Missing Positive
Given an unsorted integer array, find the first missing positive integer.

Example
Given [1,2,0] return 3, and [3,4,-1,1] return 2.

Challenge
Your algorithm should run in O(n) time and uses constant space.

 

题解

容易想到的方案是先排序,然后遍历求得缺的最小整数。排序算法中常用的基于比较的方法时间复杂度的理论下界为 $$O(n \log n)$$, 不符题目要求。常见的能达到线性时间复杂度的排序算法有 基数排序,计数排序 和 桶排序。

基数排序显然不太适合这道题,计数排序对元素落在一定区间且重复值较多的情况十分有效,且需要额外的 $$O(n)$$ 空间,对这道题不太合适。最后就只剩下桶排序了,桶排序通常需要按照一定规则将值放入桶中,一般需要额外的 $$O(n)$$ 空间,咋看一下似乎不太适合在这道题中使用,但是若能设定一定的规则原地交换原数组的值呢?这道题的难点就在于这种规则的设定。

设想我们对给定数组使用桶排序的思想排序,第一个桶放1,第二个桶放2,如果找不到相应的数,则相应的桶的值不变(可能为负值,也可能为其他值)。

那么怎么才能做到原地排序呢?即若 $$A[i] = x$$, 则将 x 放到它该去的地方 - $$A[x - 1] = x$$, 同时将原来 $$A[x - 1]$$ 地方的值交换给 $$A[i]$$.

排好序后遍历桶,如果不满足 $$f[i] = i + 1$$, 那么警察叔叔就是它了!如果都满足条件怎么办?那就返回给定数组大小再加1呗。

C++

class Solution {
public:
    /**
     * @param A: a vector of integers
     * @return: an integer
     */
    int firstMissingPositive(vector A) {
        const int size = A.size();

        for (int i = 0; i < size; ++i) {
            while (A[i] > 0 && A[i] <= size && \
                  (A[i] != i + 1) && (A[i] != A[A[i] - 1])) {
                int temp = A[A[i] - 1];
                A[A[i] - 1] = A[i];
                A[i] = temp;
            }
        }

        for (int i = 0; i < size; ++i) {
            if (A[i] != i + 1) {
                return i + 1;
            }
        }

        return size + 1;
    }
};

源码分析

核心代码为那几行交换,但是要很好地处理各种边界条件则要下一番功夫了,要能正常的交换,需满足以下几个条件:

  1. A[i] 为正数,负数和零都无法在桶中找到生存空间...
  2. A[i] \leq size 当前索引处的值不能比原数组容量大,大了的话也没用啊,肯定不是缺的第一个正数。
  3. A[i] != i + 1, 都满足条件了还交换个毛线,交换也是自身的值。
  4. A[i] != A[A[i] - 1], 避免欲交换的值和自身相同,否则有重复值时会产生死循环。

如果满足以上四个条件就可以愉快地交换彼此了,使用while循环处理,此时i并不自增,直到将所有满足条件的索引处理完。

注意交换的写法,若写成

int temp = A[i];
A[i] = A[A[i] - 1];
A[A[i] - 1] = temp;

这又是满满的 bug :( 因为在第三行中A[i]已不再是之前的值,第二行赋值时已经改变,故源码中的写法比较安全。

最后遍历桶排序后的数组,若在数组大小范围内找到不满足条件的解,直接返回,否则就意味着原数组给的元素都是从1开始的连续正整数,返回数组大小加1即可。

复杂度分析

「桶排序」需要遍历一次原数组,考虑到while循环也需要一定次数的遍历,故时间复杂度至少为 $$O(n)$$. 最后求索引值最多遍历一次排序后数组,时间复杂度最高为 $$O(n)$$, 用到了temp作为中间交换变量,空间复杂度为 $$O(1)$$.

Reference

  • Find First Missing Positive | N00tc0d3r
  • LeetCode: First Missing Positive 解题报告 - Yu's Garden - 博客园
  • First Missing Positive | 九章算法

 

9、2 Sum

Question

  • leetcode: Two Sum | LeetCode OJ
  • lintcode: (56) 2 Sum

Problem Statement

Given an array of integers, find two numbers such that they add up to a specific target number.

The function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are NOT zero-based.

Example

numbers=[2, 7, 11, 15], target=9

return [1, 2]

Note

You may assume that each input would have exactly one solution

Challenge

Either of the following solutions are acceptable:

  • O(n) Space, O(nlogn) Time
  • O(n) Space, O(n) Time

题解1 - 哈希表

找两数之和是否为target, 如果是找数组中一个值为target该多好啊!遍历一次就知道了,我只想说,too naive... 难道要将数组中所有元素的两两组合都求出来与target比较吗?时间复杂度显然为 $$O(n^2)$$, 显然不符题目要求。找一个数时直接遍历即可,那么可不可以将两个数之和转换为找一个数呢?我们先来看看两数之和为target所对应的判断条件—— $$x_i + x_j = target$$, 可进一步转化为 $$x_i = target - x_j$$, 其中 $$i$$$$j$$ 为数组中的下标。一段神奇的数学推理就将找两数之和转化为了找一个数是否在数组中了!可见数学是多么的重要...

基本思路有了,现在就来看看怎么实现,显然我们需要额外的空间(也就是哈希表)来保存已经处理过的 $$x_j$$(注意这里并不能先初始化哈希表,否则无法排除两个相同的元素相加为 target 的情况), 如果不满足等式条件,那么我们就往后遍历,并把之前的元素加入到哈希表中,如果target减去当前索引后的值在哈希表中找到了,那么就将哈希表中相应的索引返回,大功告成!

Python

class Solution:
    """
    @param numbers : An array of Integer
    @param target : target = numbers[index1] + numbers[index2]
    @return : [index1 + 1, index2 + 1] (index1 < index2)
    """
    def twoSum(self, numbers, target):
        hashdict = {}
        for i, item in enumerate(numbers):
            if (target - item) in hashdict:
                return (hashdict[target - item] + 1, i + 1)
            hashdict[item] = i

        return (-1, -1)

C++

class Solution {
public:
    /*
     * @param numbers : An array of Integer
     * @param target : target = numbers[index1] + numbers[index2]
     * @return : [index1+1, index2+1] (index1 < index2)
     */
    vector twoSum(vector &nums, int target) {
        vector result;
        const int length = nums.size();
        if (0 == length) {
            return result;
        }

        // first value, second index
        unordered_map hash(length);
        for (int i = 0; i != length; ++i) {
            if (hash.find(target - nums[i]) != hash.end()) {
                result.push_back(hash[target - nums[i]]);
                result.push_back(i + 1);
                return result;
            } else {
                hash[nums[i]] = i + 1;
            }
        }

        return result;
    }
};

Java

public class Solution {
    /*
     * @param numbers : An array of Integer
     * @param target : target = numbers[index1] + numbers[index2]
     * @return : [index1 + 1, index2 + 1] (index1 < index2)
     */
    public int[] twoSum(int[] numbers, int target) {
        if (numbers == null || numbers.length == 0) return new int[]{0, 0};

        Map hashmap = new HashMap();
        int index1 = 0, index2 = 0;
        for (int i = 0; i < numbers.length; i++) {
            if (hashmap.containsKey(target - numbers[i])) {
                index1 = hashmap.get(target - numbers[i]);
                index2 = i;
                return new int[]{1 + index1, 1 + index2};
            } else {
                hashmap.put(numbers[i], i);
            }
        }

        return new int[]{0, 0};
    }
}

源码分析

  1. 异常处理。
  2. 使用 C++ 11 中的哈希表实现unordered_map映射值和索引。Python 中的dict就是天然的哈希表。
  3. 找到满足条件的解就返回,找不到就加入哈希表中。注意题中要求返回索引值的含义。

复杂度分析

哈希表用了和数组等长的空间,空间复杂度为 $$O(n)$$, 遍历一次数组,时间复杂度为 $$O(n)$$.

题解2 - 排序后使用两根指针

但凡可以用空间换时间的做法,往往也可以使用时间换空间。另外一个容易想到的思路就是先对数组排序,然后使用两根指针分别指向首尾元素,逐步向中间靠拢,直至找到满足条件的索引为止。

C++

class Solution {
public:
    /*
     * @param numbers : An array of Integer
     * @param target : target = numbers[index1] + numbers[index2]
     * @return : [index1+1, index2+1] (index1 < index2)
     */
    vector twoSum(vector &nums, int target) {
        vector result;
        const int length = nums.size();
        if (0 == length) {
            return result;
        }

        // first num, second is index
        vector > num_index(length);
        // map num value and index
        for (int i = 0; i != length; ++i) {
            num_index[i].first = nums[i];
            num_index[i].second = i + 1;
        }

        sort(num_index.begin(), num_index.end());
        int start = 0, end = length - 1;
        while (start < end) {
            if (num_index[start].first + num_index[end].first > target) {
                --end;
            } else if(num_index[start].first + num_index[end].first == target) {
                int min_index = min(num_index[start].second, num_index[end].second);
                int max_index = max(num_index[start].second, num_index[end].second);
                result.push_back(min_index);
                result.push_back(max_index);
                return result;
            } else {
                ++start;
            }
        }

        return result;
    }
};

源码分析

  1. 异常处理。
  2. 使用length保存数组的长度,避免反复调用nums.size()造成性能损失。
  3. 使用pair组合排序前的值和索引,避免排序后找不到原有索引信息。
  4. 使用标准库函数排序。
  5. 两根指针指头尾,逐步靠拢。

复杂度分析

遍历一次原数组得到pair类型的新数组,时间复杂度为 $$O(n)$$, 空间复杂度也为 $$O(n)$$. 标准库中的排序方法时间复杂度近似为 $$O(n \log n)$$, 两根指针遍历数组时间复杂度为 $$O(n)$$.

10、3 Sum

Question

  • leetcode: 3Sum | LeetCode OJ
  • lintcode: (57) 3 Sum
Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0?
Find all unique triplets in the array which gives the sum of zero.

Example
For example, given array S = {-1 0 1 2 -1 -4}, A solution set is:

(-1, 0, 1)
(-1, -1, 2)
Note
Elements in a triplet (a,b,c) must be in non-descending order. (ie, a ≤ b ≤ c)

The solution set must not contain duplicate triplets.

 

题解1 - 排序 + 哈希表 + 2 Sum

相比之前的 2 Sum, 3 Sum 又多加了一个数,按照之前 2 Sum 的分解为『1 Sum + 1 Sum』的思路,我们同样可以将 3 Sum 分解为『1 Sum + 2 Sum』的问题,具体就是首先对原数组排序,排序后选出第一个元素,随后在剩下的元素中使用 2 Sum 的解法。

Python

class Solution:
    """
    @param numbersbers : Give an array numbersbers of n integer
    @return : Find all unique triplets in the array which gives the sum of zero.
    """
    def threeSum(self, numbers):
        triplets = []
        length = len(numbers)
        if length < 3:
            return triplets

        numbers.sort()
        for i in xrange(length):
            target = 0 - numbers[i]
            # 2 Sum
            hashmap = {}
            for j in xrange(i + 1, length):
                item_j = numbers[j]
                if (target - item_j) in hashmap:
                    triplet = [numbers[i], target - item_j, item_j]
                    if triplet not in triplets:
                        triplets.append(triplet)
                else:
                    hashmap[item_j] = j

        return triplets

源码分析

  1. 异常处理,对长度小于3的直接返回。
  2. 排序输入数组,有助于提高效率和返回有序列表。
  3. 循环遍历排序后数组,先取出一个元素,随后求得 2 Sum 中需要的目标数。
  4. 由于本题中最后返回结果不能重复,在加入到最终返回值之前查重。

由于排序后的元素已经按照大小顺序排列,且在2 Sum 中先遍历的元素较小,所以无需对列表内元素再排序。

复杂度分析

排序时间复杂度 $$O(n \log n)$$, 两重for循环,时间复杂度近似为 $$O(n^2)$$,使用哈希表(字典)实现,空间复杂度为 $$O(n)$$.

目前这段源码为比较简易的实现,leetcode 上的运行时间为500 + ms, 还有较大的优化空间,嗯,后续再进行优化。

C++

class Solution {
public:
    vector > threeSum(vector &num) 
    {
        vector > result;
        if (num.size() < 3) return result;

        int ans = 0;

        sort(num.begin(), num.end());

        for (int i = 0;i < num.size() - 2; ++i)
        {
            if (i > 0 && num[i] == num[i - 1])  
                continue;
            int j = i + 1;
            int k = num.size() - 1;

            while (j < k)
            {
                ans = num[i] + num[j] + num[k];

                if (ans == 0)
                {
                    result.push_back({num[i], num[j], num[k]});
                    ++j;
                    while (j < num.size() && num[j] == num[j - 1])
                        ++j;
                    --k;
                    while (k >= 0 && num[k] == num[k + 1])
                        --k;
                }
                else if (ans > 0) 
                    --k;
                else 
                    ++j;
            }
        }

        return result;
    }
};

源码分析

同python解法不同,没有使用hash map

S = {-1 0 1 2 -1 -4}
排序后:
S = {-4 -1 -1 0 1 2}
      ↑  ↑        ↑
      i  j        k
         →        ←
i每轮只走一步,j和k根据S[i]+S[j]+S[k]=ans和0的关系进行移动,且j只向后走(即S[j]只增大),k只向前走(即S[k]只减小)
如果ans>0说明S[k]过大,k向前移;如果ans<0说明S[j]过小,j向后移;ans==0即为所求。
至于如何取到所有解,看代码即可理解,不再赘述。

复杂度分析

外循环i走了n轮,每轮j和k一共走n-i步,所以时间复杂度为$$O(n^2)$$。 最终运行时间为52ms

Reference

  • 3Sum | 九章算法
  • A simply Python version based on 2sum - O(n^2) - Leetcode Discuss

 

11、3 Sum Closest

Question

  • leetcode: 3Sum Closest | LeetCode OJ
  • lintcode: (59) 3 Sum Closest
Given an array S of n integers, find three integers in S such that the sum is closest to a given number, target. 
Return the sum of the three integers. You may assume that each input would have exactly one solution.

For example, given array S = {-1 2 1 -4}, and target = 1.
The sum that is closest to the target is 2. (-1 + 2 + 1 = 2).

 

题解1 - 排序 + 2 Sum + 两根指针 + 优化过滤

和 3 Sum 的思路接近,首先对原数组排序,随后将3 Sum 的题拆解为『1 Sum + 2 Sum』的题,对于 Closest 的题使用两根指针而不是哈希表的方法较为方便。对于有序数组来说,在查找 Cloest 的值时其实是有较大的优化空间的。

Python

class Solution:
    """
    @param numbers: Give an array numbers of n integer
    @param target : An integer
    @return : return the sum of the three integers, the sum closest target.
    """
    def threeSumClosest(self, numbers, target):
        result = 2**31 - 1
        length = len(numbers)
        if length < 3:
            return result

        numbers.sort()
        larger_count = 0
        for i, item_i in enumerate(numbers):
            start = i + 1
            end = length - 1
            # optimization 1 - filter the smallest sum greater then target
            if start < end:
                sum3_smallest = numbers[start] + numbers[start + 1] + item_i
                if sum3_smallest > target:
                    larger_count += 1
                    if larger_count > 1:
                        return result

            while (start < end):
                sum3 = numbers[start] + numbers[end] + item_i
                if abs(sum3 - target) < abs(result - target):
                    result = sum3

                # optimization 2 - filter the sum3 closest to target
                sum_flag = 0
                if sum3 > target:
                    end -= 1
                    if sum_flag == -1:
                        break
                    sum_flag = 1
                elif sum3 < target:
                    start += 1
                    if sum_flag == 1:
                        break
                    sum_flag = -1
                else:
                    return result

        return result

源码分析

  1. leetcode 上不让自己导入sys包,保险起见就初始化了result为还算较大的数,作为异常的返回值。
  2. 对数组进行排序。
  3. 依次遍历排序后的数组,取出一个元素item_i后即转化为『2 Sum Cloest』问题。『2 Sum Cloest』的起始元素索引为i + 1,之前的元素不能参与其中。
  4. 优化一——由于已经对原数组排序,故遍历原数组时比较最小的三个元素和target值,若第二次大于target果断就此罢休,后面的值肯定越来越大。
  5. 两根指针求『2 Sum Cloest』,比较sum3resulttarget的差值的绝对值,更新result为较小的绝对值。
  6. 再度对『2 Sum Cloest』进行优化,仍然利用有序数组的特点,若处于『一大一小』的临界值时就可以马上退出了,后面的元素与target之差的绝对值只会越来越大。

复杂度分析

对原数组排序,平均时间复杂度为 $$O(n \log n)$$, 两重for循环,由于有两处优化,故最坏的时间复杂度才是 $$O(n^2)$$, 使用了result作为临时值保存最接近target的值,两处优化各使用了一个辅助变量,空间复杂度 $$O(1)$$.

C++

class Solution {
public:
    int threeSumClosest(vector &num, int target) 
    {
        if (num.size() <= 3) return accumulate(num.begin(), num.end(), 0);
        sort (num.begin(), num.end());

        int result = 0, n = num.size(), temp;
        result = num[0] + num[1] + num[2];
        for (int i = 0; i < n - 2; ++i)
        {
            int j = i + 1, k = n - 1;
            while (j < k)
            {
                temp = num[i] + num[j] + num[k];

                if (abs(target - result) > abs(target - temp))
                    result = temp;
                if (result == target)
                    return result;
                ( temp > target ) ? --k : ++j;
            }
        }
        return result;
    }
};

源码分析

和前面3Sum解法相似,同理使用i,j,k三个指针进行循环。
区别在于3sum中的target为0,这里新增一个变量用于比较哪组数据与target更为相近

复杂度分析

时间复杂度同理为$$O(n^2)$$ 运行时间 16ms

Reference

  • 3Sum Closest | 九章算法

12、Remove Duplicates from Sorted Array

Question

  • leetcode: Remove Duplicates from Sorted Array | LeetCode OJ
  • lintcode: (100) Remove Duplicates from Sorted Array
Given a sorted array, remove the duplicates in place
such that each element appear only once and return the new length.

Do not allocate extra space for another array,
you must do this in place with constant memory.

For example,
Given input array A = [1,1,2],

Your function should return length = 2, and A is now [1,2].

Example

题解

使用两根指针(下标),一个指针(下标)遍历数组,另一个指针(下标)只取不重复的数置于原数组中。

C++

class Solution {
public:
    /**
     * @param A: a list of integers
     * @return : return an integer
     */
    int removeDuplicates(vector &nums) {
        if (nums.size() <= 1) return nums.size();

        int len = nums.size();
        int newIndex = 0;
        for (int i = 1; i< len; ++i) {
            if (nums[i] != nums[newIndex]) {
                newIndex++;
                nums[newIndex] = nums[i];
            }
        }

        return newIndex + 1;
    }
};

Java

public class Solution {
    /**
     * @param A: a array of integers
     * @return : return an integer
     */
    public int removeDuplicates(int[] nums) {
        if (nums == null) return -1;
        if (nums.length <= 1) return nums.length;

        int newIndex = 0;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] != nums[newIndex]) {
                newIndex++;
                nums[newIndex] = nums[i];
            }
        }

        return newIndex + 1;
    }
}

源码分析

注意最后需要返回的是索引值加1。

复杂度分析

遍历一次数组,时间复杂度 $$O(n)$$, 空间复杂度 $$O(1)$$.

13、Remove Duplicates from Sorted Array II

Question

  • leetcode: Remove Duplicates from Sorted Array II | LeetCode OJ
  • lintcode: (101) Remove Duplicates from Sorted Array II
Follow up for "Remove Duplicates":
What if duplicates are allowed at most twice?

For example,
Given sorted array A = [1,1,1,2,2,3],

Your function should return length = 5, and A is now [1,1,2,2,3].
Example

题解

在上题基础上加了限制条件元素最多可重复出现两次。因此可以在原题的基础上添加一变量跟踪元素重复出现的次数,小于指定值时执行赋值操作。但是需要注意的是重复出现次数occurence的初始值(从1开始,而不是0)和reset的时机。这种方法比较复杂,谢谢 @meishenme 提供的简洁方法,核心思想仍然是两根指针,只不过此时新索引自增的条件是当前遍历的数组值和『新索引』或者『新索引-1』两者之一不同。

C++

class Solution {
public:
    /**
     * @param A: a list of integers
     * @return : return an integer
     */
    int removeDuplicates(vector &nums) {
        if (nums.size() <= 2) return nums.size();

        int len = nums.size();
        int newIndex = 1;
        for (int i = 2; i < len; ++i) {
            if (nums[i] != nums[newIndex] || nums[i] != nums[newIndex - 1]) {
                ++newIndex;
                nums[newIndex] = nums[i];
            }
        }

        return newIndex + 1;
    }
};

Java

public class Solution {
    /**
     * @param A: a array of integers
     * @return : return an integer
     */
    public int removeDuplicates(int[] nums) {
        if (nums == null) return -1;
        if (nums.length <= 2) return nums.length;

        int newIndex = 1;
        for (int i = 2; i < nums.length; i++) {
            if (nums[i] != nums[newIndex] || nums[i] != nums[newIndex - 1]) {
                newIndex++;
                nums[newIndex] = nums[i];
            }
        }

        return newIndex + 1;
    }
}

源码分析

遍历数组时 i 从2开始,newIndex 初始化为1便于分析。

复杂度分析

时间复杂度 $$O(n)$$, 空间复杂度 $$O(1)$$.

14、Merge Sorted Array

Question

  • leetcode: Merge Sorted Array | LeetCode OJ
  • lintcode: (6) Merge Sorted Array
Given two sorted integer arrays A and B, merge B into A as one sorted array.

Example
A = [1, 2, 3, empty, empty], B = [4, 5]

After merge, A will be filled as [1, 2, 3, 4, 5]

Note
You may assume that A has enough space (size that is greater or equal to m + n)
to hold additional elements from B.
The number of elements initialized in A and B are m and n respectively.

题解

因为本题有 in-place 的限制,故必须从数组末尾的两个元素开始比较;否则就会产生挪动,一旦挪动就会是 $$O(n^2)$$ 的。 自尾部向首部逐个比较两个数组内的元素,取较大的置于数组 A 中。由于 A 的容量较 B 大,故最后 m == 0 或者 n == 0 时仅需处理 B 中的元素,因为 A 中的元素已经在 A 中,无需处理。

Python

class Solution:
    """
    @param A: sorted integer array A which has m elements,
              but size of A is m+n
    @param B: sorted integer array B which has n elements
    @return: void
    """
    def mergeSortedArray(self, A, m, B, n):
        if B is None:
            return A

        index = m + n - 1
        while m > 0 and n > 0:
            if A[m - 1] > B[n - 1]:
                A[index] = A[m - 1]
                m -= 1
            else:
                A[index] = B[n - 1]
                n -= 1
            index -= 1

        # B has elements left
        while n > 0:
            A[index] = B[n - 1]
            n -= 1
            index -= 1

C++

class Solution {
public:
    /**
     * @param A: sorted integer array A which has m elements,
     *           but size of A is m+n
     * @param B: sorted integer array B which has n elements
     * @return: void
     */
    void mergeSortedArray(int A[], int m, int B[], int n) {
        int index = m + n - 1;
        while (m > 0 && n > 0) {
            if (A[m - 1] > B[n - 1]) {
                A[index] = A[m - 1];
                --m;
            } else {
                A[index] = B[n - 1];
                --n;
            }
            --index;
        }

        // B has elements left
        while (n > 0) {
            A[index] = B[n - 1];
            --n;
            --index;
        }
    }
};

Java

class Solution {
    /**
     * @param A: sorted integer array A which has m elements,
     *           but size of A is m+n
     * @param B: sorted integer array B which has n elements
     * @return: void
     */
    public void mergeSortedArray(int[] A, int m, int[] B, int n) {
        if (A == null || B == null) return;

        int index = m + n - 1;
        while (m > 0 && n > 0) {
            if (A[m - 1] > B[n - 1]) {
                A[index] = A[m - 1];
                m--;
            } else {
                A[index] = B[n - 1];
                n--;
            }
            index--;
        }

        // B has elements left
        while (n > 0) {
            A[index] = B[n - 1];
            n--;
            index--;
        }
    }
}

源码分析

第一个 while 只能用条件与。

复杂度分析

最坏情况下需要遍历两个数组中所有元素,时间复杂度为 $$O(n)$$. 空间复杂度 $$O(1)$$.

15、Merge Sorted Array II

Question

  • lintcode: (64) Merge Sorted Array II
Merge two given sorted integer array A and B into a new sorted integer array.

Example
A=[1,2,3,4]

B=[2,4,5,6]

return [1,2,2,3,4,4,5,6]

Challenge
How can you optimize your algorithm
if one array is very large and the other is very small?

题解

上题要求 in-place, 此题要求返回新数组。由于可以生成新数组,故使用常规思路按顺序遍历即可。

Python

class Solution:
    #@param A and B: sorted integer array A and B.
    #@return: A new sorted integer array
    def mergeSortedArray(self, A, B):
        if A is None or len(A) == 0:
            return B
        if B is None or len(B) == 0:
            return A

        C = []
        aLen, bLen = len(A), len(B)
        i, j = 0, 0
        while i < aLen and j < bLen:
            if A[i] < B[j]:
                C.append(A[i])
                i += 1
            else:
                C.append(B[j])
                j += 1

        # A has elements left
        while i < aLen:
            C.append(A[i])
            i += 1

        # B has elements left
        while j < bLen:
            C.append(B[j])
            j += 1

        return C

C++

class Solution {
public:
    /**
     * @param A and B: sorted integer array A and B.
     * @return: A new sorted integer array
     */
    vector mergeSortedArray(vector &A, vector &B) {
        if (A.empty()) return B;
        if (B.empty()) return A;

        int aLen = A.size(), bLen = B.size();
        vector C;
        int i = 0, j = 0;
        while (i < aLen && j < bLen) {
            if (A[i] < B[j]) {
                C.push_back(A[i]);
                ++i;
            } else {
                C.push_back(B[j]);
                ++j;
            }
        }

        // A has elements left
        while (i < aLen) {
            C.push_back(A[i]);
            ++i;
        }

        // B has elements left
        while (j < bLen) {
            C.push_back(B[j]);
            ++j;
        }

        return C;
    }
};

Java

class Solution {
    /**
     * @param A and B: sorted integer array A and B.
     * @return: A new sorted integer array
     */
    public ArrayList mergeSortedArray(ArrayList A, ArrayList B) {
        if (A == null || A.isEmpty()) return B;
        if (B == null || B.isEmpty()) return A;

        ArrayList C = new ArrayList();
        int aLen = A.size(), bLen = B.size();
        int i = 0, j = 0;
        while (i < aLen && j < bLen) {
            if (A.get(i) < B.get(j)) {
                C.add(A.get(i));
                i++;
            } else {
                C.add(B.get(j));
                j++;
            }
        }

        // A has elements left
        while (i < aLen) {
            C.add(A.get(i));
            i++;
        }

        // B has elements left
        while (j < bLen) {
            C.add(B.get(j));
            j++;
        }

        return C;
    }
}

源码分析

分三步走,后面分别单独处理剩余的元素。

复杂度分析

遍历 A, B 数组各一次,时间复杂度 $$O(n)$$, 空间复杂度 $$O(1)$$.

Challenge

两个倒排列表,一个特别大,一个特别小,如何 Merge?此时应该考虑用一个二分法插入小的,即内存拷贝。

16、Median

Question

  • lintcode: (80) Median
Given a unsorted array with integers, find the median of it.

A median is the middle number of the array after it is sorted.

If there are even numbers in the array, return the N/2-th number after sorted.

Example
Given [4, 5, 1, 2, 3], return 3

Given [7, 9, 4, 5], return 5

Challenge
O(n) time.

题解

寻找未排序数组的中位数,简单粗暴的方法是先排序后输出中位数索引处的数,但是基于比较的排序算法的时间复杂度为 $$O(n \log n)$$, 不符合题目要求。线性时间复杂度的排序算法常见有计数排序、桶排序和基数排序,这三种排序方法的空间复杂度均较高,且依赖于输入数据特征(数据分布在有限的区间内),用在这里并不是比较好的解法。

由于这里仅需要找出中位数,即找出数组中前半个长度的较大的数,不需要进行完整的排序,说到这你是不是想到了快速排序了呢?快排的核心思想就是以基准为界将原数组划分为左小右大两个部分,用在这十分合适。快排的实现见 Quick Sort, 由于调用一次快排后基准元素的最终位置是知道的,故递归的终止条件即为当基准元素的位置(索引)满足中位数的条件时(左半部分长度为原数组长度一半)即返回最终结果。由于函数原型中左右最小索引并不总是原数组的最小最大,故需要引入相对位置(长度)也作为其中之一的参数。若左半部分长度偏大,则下一次递归排除右半部分,反之则排除左半部分。

Python

class Solution:
    """
    @param nums: A list of integers.
    @return: An integer denotes the middle number of the array.
    """
    def median(self, nums):
        if not nums:
            return -1
        return self.helper(nums, 0, len(nums) - 1, (1 + len(nums)) / 2)

    def helper(self, nums, l, u, size):
        if l >= u:
            return nums[u]

        m = l
        for i in xrange(l + 1, u + 1):
            if nums[i] < nums[l]:
                m += 1
                nums[m], nums[i] = nums[i], nums[m]

        # swap between m and l after partition, important!
        nums[m], nums[l] = nums[l], nums[m]

        if m - l + 1 == size:
            return nums[m]
        elif m - l + 1 > size:
            return self.helper(nums, l, m - 1, size)
        else:
            return self.helper(nums, m + 1, u, size - (m - l + 1))

C++

class Solution {
public:
    /**
     * @param nums: A list of integers.
     * @return: An integer denotes the middle number of the array.
     */
    int median(vector &nums) {
        if (nums.empty()) return 0;

        int len = nums.size();
        return helper(nums, 0, len - 1, (len + 1) / 2);
    }

private:
    int helper(vector &nums, int l, int u, int size) {
        // if (l >= u) return nums[u];

        int m = l; // index m to track pivot
        for (int i = l + 1; i <= u; ++i) {
            if (nums[i] < nums[l]) {
                ++m;
                int temp = nums[i];
                nums[i] = nums[m];
                nums[m] = temp;
            }
        }

        // swap with the pivot
        int temp = nums[m];
        nums[m] = nums[l];
        nums[l] = temp;

        if (m - l + 1 == size) {
            return nums[m];
        } else if (m - l + 1 > size) {
            return helper(nums, l, m - 1, size);
        } else {
            return helper(nums, m + 1, u, size - (m - l + 1));
        }
    }
};

Java

public class Solution {
    /**
     * @param nums: A list of integers.
     * @return: An integer denotes the middle number of the array.
     */
    public int median(int[] nums) {
        if (nums == null) return -1;

        return helper(nums, 0, nums.length - 1, (nums.length + 1) / 2);
    }

    // l: lower, u: upper, m: median
    private int helper(int[] nums, int l, int u, int size) {
        if (l >= u) return nums[u];

        int m = l;
        for (int i = l + 1; i <= u; i++) {
            if (nums[i] < nums[l]) {
                m++;
                int temp = nums[m];
                nums[m] = nums[i];
                nums[i] = temp;
            }
        }
        // swap between array[m] and array[l]
        // put pivot in the mid
        int temp = nums[m];
        nums[m] = nums[l];
        nums[l] = temp;

        if (m - l + 1 == size) {
            return nums[m];
        } else if (m - l + 1 > size) {
            return helper(nums, l, m - 1, size);
        } else {
            return helper(nums, m + 1, u, size - (m - l + 1));
        }
    }
}

源码分析

以相对距离(长度)进行理解,递归终止步的条件一直保持不变(比较左半部分的长度)。

以题目中给出的样例进行分析,size 传入的值可为(len(nums) + 1) / 2, 终止条件为m - l + 1 == size, 含义为基准元素到索引为l的元素之间(左半部分)的长度(含)与(len(nums) + 1) / 2相等。若m - l + 1 > size, 即左半部分长度偏大,此时递归终止条件并未变化,因为l的值在下一次递归调用时并未改变,所以仍保持为size; 若m - l + 1 < size, 左半部分长度偏小,下一次递归调用右半部分,由于此时左半部分的索引值已变化,故size应改为下一次在右半部分数组中的终止条件size - (m - l + 1), 含义为原长度size减去左半部分数组的长度m - l + 1.

复杂度分析

和快排类似,这里也有最好情况与最坏情况,平均情况下,索引m每次都处于中央位置,即每次递归后需要遍历的数组元素个数减半,故总的时间复杂度为 $$O(n (1 + 1/2 + 1/4 + ...)) = O(2n)$$, 最坏情况下为平方。使用了临时变量,空间复杂度为 $$O(1)$$, 满足题目要求。

17、Partition Array by Odd and Even

Question

  • lintcode: (373) Partition Array by Odd and Even
  • Segregate Even and Odd numbers - GeeksforGeeks
Partition an integers array into odd number first and even number second.

Example
Given [1, 2, 3, 4], return [1, 3, 2, 4]

Challenge
Do it in-place.

题解

将数组中的奇数和偶数分开,使用『两根指针』的方法最为自然,奇数在前,偶数在后,若不然则交换之。

Java

public class Solution {
    /**
     * @param nums: an array of integers
     * @return: nothing
     */
    public void partitionArray(int[] nums) {
        if (nums == null) return;

        int left = 0, right = nums.length - 1;
        while (left < right) {
            // odd number
            while (left < right && nums[left] % 2 != 0) {
                left++;
            }
            // even number
            while (left < right && nums[right] % 2 == 0) {
                right--;
            }
            // swap
            if (left < right) {
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
            }
        }
    }
}

C++

  void partitionArray(vector &nums) {
        if (nums.empty()) return;

        int i=0, j=nums.size()-1;
        while (i

源码分析

注意处理好边界即循环时保证left < right.

复杂度分析

遍历一次数组,时间复杂度为 $$O(n)$$, 使用了两根指针,空间复杂度 $$O(1)$$.

18、Kth Largest Element

Question

  • leetcode: Kth Largest Element in an Array | LeetCode OJ
  • lintcode: (5) Kth Largest Element
Find K-th largest element in an array.

Example
In array [9,3,2,4,8], the 3rd largest element is 4.

In array [1,2,3,4,5], the 1st largest element is 5,
2nd largest element is 4, 3rd largest element is 3 and etc.

Note
You can swap elements in the array

Challenge
O(n) time, O(1) extra memory.

题解

找第 K 大数,基于比较的排序的方法时间复杂度为 $$O(n)$$, 数组元素无区间限定,故无法使用线性排序。由于只是需要找第 K 大数,这种类型的题通常需要使用快排的思想解决。Quick Sort 总结了一些经典模板。这里比较基准值最后的位置的索引值和 K 的大小关系即可递归求解。

Java

class Solution {
    //param k : description of k
    //param numbers : array of numbers
    //return: description of return
    public int kthLargestElement(int k, ArrayList numbers) {
        if (numbers == null || numbers.isEmpty()) return -1;

        int result = qSort(numbers, 0, numbers.size() - 1, k);
        return result;
    }

    private int qSort(ArrayList nums, int l, int u, int k) {
        // l should not greater than u
        if (l >= u) return nums.get(u);

        // index m of nums
        int m = l;
        for (int i = l + 1; i <= u; i++) {
            if (nums.get(i) > nums.get(l)) {
                m++;
                Collections.swap(nums, m, i);
            }
        }
        Collections.swap(nums, m, l);

        if (m + 1 == k) {
            return nums.get(m);
        } else if (m + 1 > k) {
            return qSort(nums, l, m - 1, k);
        } else {
            return qSort(nums, m + 1, u, k);
        }
    }
}

源码分析

递归的终止条件有两个,一个是左边界的值等于右边界(实际中其实不会有 l > u), 另一个则是索引值 m + 1 == k. 这里找的是第 K 大数,故为降序排列,for 循环中使用nums.get(i) > nums.get(l) 而不是小于号。

复杂度分析

最坏情况下需要遍历 $$ n + n - 1 + ... + 1 = O(n^2)$$, 平均情况下 $$n + n/2 + n/4 + ... + 1 = O(2n)=O(n)$$. 故平均情况时间复杂度为 $$O(n)$$. 交换数组的值时使用了额外空间,空间复杂度 $$O(1)$$.

 

你可能感兴趣的:(问题)