数组划分 & 合并排序数组

考察内容:链表,双指针

时间:2020-09-18

作者:guuzaa

实现语言: C++

掘金主页:🌏

题目 1 : 数组划分

题目链接 🔗

给一个数组和整数 k,让我们划分这个数组,把「小于」 k 的元素都移到左边,「大于等于」 k 的元素移到右边,并返回划分位置 i(nums[i] 是数组中第一个大于等于k的元素),要真正的划分数组。

分析 💻

读题之后,觉得跟快速排序的 partition 操作有点像,但是 pivot 不再是数组中第一个元素而是大于等于 k 的最小元素。所以要想解决这个问题,首先要找到数组中「大于等于」k 的最小元素。

关于 k 有三种情况需要分别讨论:

  1. k 小于数组中的所有元素,显然结果是 0,不需要对数组操作。
  2. k 大于数组中的所有元素,结果返回 nums.length(题设也给出了),同样也不需要对数组操作。
  3. k 介于数组元素最大值和最小值之间,执行 partition 操作并返回 pivot 的最终位置。

先用代码实现一下

// 返回数组中大于等于k最小元素的位置,否则返回-1(针对情况2)
int find(vector<int> nums, int k) {
	int pos = -1;
	int right_bound = INT_MAX;
	for (int i = 0; i < (int) nums.size(); i++) {
		if (nums[i] == k) return i;
		else if (nums[i] > k && nums[i] < right_bound) {
			pos = i;
			right_bound = nums[i];
		}
	}
	return pos;
}

但是这种实现方式有一个问题:k 小于数组所有元素和 k 恰巧是数组最小元素,两种情况的返回值相同。这会对情况 1 下的元组进行划分操作,违反题意。改进代码如下:

int find(vector<int> nums, int k) {
    int pos = -1;
    int right_bound = INT_MAX;
    int cnt = 0;
    int len = (int) nums.size();
    
    for (int i = 0; i < len; i++) {
        if (nums[i] == k) return k;
        
        if (nums[i] > k) {
            cnt += 1;
            if (nums[i] < right_bound) {  // k < nums[i] < right_bound
                pos = i;
                right_bound = nums[i];
            }
        }
    }
    if (cnt == len) return -1; // 对应情况1 小于所有元素
    else {
        if (pos == -1) return len; // 对应情况2 大于所有元素
        else return pos;  // 对应情况3 
    }
    // 或者用三目运算符简写
    //return cnt == len ? -1 : (pos == -1) ? len : pos;
}

通过计数器 cnt 来记录数组中大于 k 的元素个数。如果 cnt 等于数组长度,说明 k 小于数组所有元素;否则根据 pos 来判断情况 2 和 3 。如果 pos 等于 -1 说明 k 大于所有元素;其余都是情况3。

实现了找到数组中「大于等于」k 的最小元素操作后,下面就是简单的 partition 操作。代码如下:

int partitionArray(vector<int> &nums, int k) {
	if (nums.empty()) return 0;  // 数组为空
	
	int pos = find(nums, k);
	if (-1 == pos) return 0;
	else if(pos == nums.size()) return pos;
	
	int pivot = nums[pos];
	swap(nums[0], nums[pos]);
	
	int left = 0;
	int right = nums.size() - 1;
	
	while (left < right) {
		while (left < right && nums[right] >= pivot) right -= 1;
		nums[left] = nums[right];
		
		while (left < right && nums[left] < pivot) left += 1;
		nums[right] = nums[left];
	}
	nums[left] = pivot;
	return left;
    // 时间复杂度
    // find 是 O(N) 
    // partitionArray 是 O(log(N)) 总的时间复杂度是 O(N)
    // 空间复杂是 O(1)
}

题目 2 : 合并排序数组

题目链接

给两个排序数组 A 和 B 以及它们各自的长度,要求把它们合并并将结果存储在数组 A 中(假设 A 长度足够)。

分析 💻

这题有趣就有趣在不是简单的将两个数组合并成一个新数组,而是存储在「原」数组 A 中。

我们先实现第一个问题(返回新数组)的常规代码。

vector<int> mergeArray(vector<int> A, vector<int> B) {
    if (A.empty()) return B;
    if (B.empty()) return A;
    
    vector<int> res;
    int left = 0;
    int right = 0;
    
    while (left < A.size() && right < B.size()) {
        if (A[left] < B[right]) res.emplace_back(A[left++]);
        else res.emplace_back(B[right++])
    }
    // 下面两个 while 循环只有一个执行
    while (left < A.size()) res.emplace_back(A[left++]);
    while (right < B.size()) res.emplace_back(B[right++]);
    
    return res;
}

那么第二个问题也不难解决,先开一个新数组,按照上面的思路求得结果。然后依次将结果中的元素赋给数组 A。但这会导致空间复杂度达到 O(m+n)。

那有没有可能在 O(1) 的空间复杂度下完成呢?也就是原地合并。

我们转换一下方向,还是用双指针的思路。不同的是,我们从数组尾部开始合并,先把值大的元素插到数组 A 的尾部,然后依次向数组头部遍历。

有的同学可能会问,这样不会覆盖数组 A 中原来的值吗?

这很容易回答,题设说数组 A 的长度足够(大于等于 m+n )。如果从尾部开始插入,那么插入的起始位置是 m+n-1。设想一个最极端的情况:B 中所有元素都大于 A 。那么 B 的元素应该全部放到 A 的尾部,即便这样也不会覆盖 A 的元素。

代码实现如下

void mergeSortedArray(int[] a, int m,int b[], int n) {
	if (n == 0) return;
	if (m == 0) {
		for (int i = 0; i < n; i++) a[i] = b[i];
	}
	
	int left = m - 1;
	int right = n - 1;
	// 这里的 left + right + 1 表示数组 A 尾部待插入位置 也就是上面说的 m + n - 1
	while (left >= 0 && right >= 0) {
		if (a[left] > b[right]) a[left + right + 1] = a[left--];
		else a[left + right + 1] = b[right--];
	}
	
	while (right >= 0) a[left + right - 1] = b[right--];
}

写完之后感觉有点太长,继续优化。

特例太复杂,那就优化特例:

  • m 等于 0 意味着 left 小于 0 ,那么第一个 while 就不会执行。第二个 while 会执行,将数组 B 的元素插入到 a 中,这跟特例中的 for 循环执行的操作相同,所以对 m 等于 0 的判断可以删掉。
  • n 等于 0 意味着 right 小于 0,那么所有 while 都不会执行,因此对 n 等于 0 的判断也可以删掉。

最终代码如下:

void mergeSortedArray(int[] a, int m,int b[], int n) {
	int left = m - 1;
	int right = n - 1;
	
	while (left >= 0 && right >= 0) {
		if (a[left] > b[right]) a[left + right + 1] = a[left--];
		else a[left + right + 1] = b[right--];
	}
	
	while (right >= 0) a[left + right - 1] = b[right--];
    
    // 空间复杂度是 O(1)
    // 时间复杂度是 O(m + n)
}

为什么在执行完第一个 while 之后,为什么没有对 left 是否大于等于 0 判断?

因为如果 left 大于等于 0 的话,那么 right 肯定小于 0。也就是说数组 B 已经全部插到数组 A 的后面去了。现在的数组 A 已经达到有序,不用进一步操作。

总结 📕:

  • 这两道题目是我这个周碰到的关于数组的算法题,看似简单,但是有很多的细节值得深究。
  • 代码优化太重要了!

全文完

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值