分治与排序(下)

目录:
1|分治思想:快速排序,归并排序
2|小和,逆序对等问题求解
3|空间结构法之:堆排序,桶排序
4|排序算法小结
5|题解

1.分治法简介:问题划分为若干个子问题而与原来问题一致的子问题,递归解决这些问题,然后再合并这些结果,就得到了原问题的解
关键点:
1.原问题可以一直分解为形式相同子问题,当子问题较小时,可以自然求解,如一个元素本身有序.
2.子问题的解通过合并可以得到原问题的解
3.子问题的分解以及解的合并一定是简单的,否则分解和合并的时间复杂度甚至超过暴力解法,得不偿失.

归并是划分很简单,合并比较复杂
快排的思想重点在划分,合并不复杂

快排,重点是划分!:
1.分解:数组A[]被划分Wie两个子数组A[p,…q-1]和A[q+1,…r]使得A[q]为大小居中的数,左侧A[p,…q-1]的每个元素斗小于等于中间元素,而右侧元素A[q+1,…r]中的每个元素都大于等于中间元素,计算下标也算是划分的一部分.
2.解决:通过递归调用快速排序,分别对于划分的左侧和划分的右侧再次使用快排
3.和序:因为划分好后,每一个元素左边的元素都小于这个元素,右边的元素都大于这个元素,所以说,和序这部分根本就不用做,前两步都做好了.

Partition方法:
方法不唯一,但是我个人觉得看一两种最简单效率最高的方法就够了,剩下的了解一下就可以了,这一选择双向移动指针法.

法一:双向扫描法:头指针从左往右扫,找到大于主元的元素,再将右指针从右往左扫,找到小于主元的元素,两者交换,直到左侧无大于主元的元素,右侧无小于主元的元素.这里直接给出代码

#include<cstdio>
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
#include<math.h>
using namespace std;

int Partition(vector<int>& A, int begin, int end) {		//问题的划分
	int record = A[begin];
	int i, j;
	i = begin, j = end;
	while (i<=j) {
		while (A[i] <= record&&i<=j) {
			++i;
		}
		while (A[j] >= record&&i<=j) {
			--j;
		}
		if (i <= j) {
			swap(A[i], A[j]);
		}
	}
	swap(A[begin], A[j]);
	return j;
}


void Quick_Sort(vector<int>& A, int begin, int end)
{
	if (begin < end) {
		int q = Partition(A, begin, end);		//问题的分解与解决
		Quick_Sort(A, begin, q - 1);
		Quick_Sort(A, q + 1, end);
	}
}

int main()
{
	vector<int> S={ 1,2,34,543,4,234,15513,242,25343,43434 };
	Quick_Sort(S, 0, S.size() - 1);
	for (vector<int>::iterator a = S.begin(); a != S.end(); ++a) {
		cout << *a << " ";
	}
	return 0;
}

这个划分思路上很是清晰,但是实际实现起来,调试起细节来足足花了我1个半小时(也就在学校才会这么认真doge),我们经过调试模拟最终情况后会发现,当情况为
<= <= >=时(此时刚刚走完一层内循环,也就是A[J]与A[I]的交换)
i 指向左边,j指向右边时,因为i的判断条件是小于等于j,所以i移动完后i=j的位置,这时,j因为所指元素>=记录元素record,所以且i=j(判断条件是i<=j),所以j向前移,也就说j+1=i,此时j所指向的元素小于record,交换A[begin]和A[j]即可

<= >= >=时,i同样是指向左边,j同样是指向了右边,因为i的判断条件是小于等于j,所以i再次移动后会指向中间的元素,j再次左移,会指向原来的i所指向的位置,所以此时的j又在i的左边并且紧邻着i,交换A[begin],A[j]即可.
上面这块比较抽象,大家最好手动模拟一下

快排优化
优化核心思想:每次取的主元(划分标准)最好可以把这个数组化分为两段大小相等的部分,如果每次都没选好,最后的时间复杂度退化为冒泡排序了.

方法一:三点(中值)优化法
方法二:绝对(中值)优化法
这里我只讲方法一:因为大部分库函数都是用三点优化法,对于方法二:绝对优化法虽然可以严格保证时间复杂度为O(nlogn)但是对于不是特别大量的测试数据,因为要先运用插入排序的思想求出数组的绝对中值而多花去O(n)的时间复杂度

int Partition(vector<int>& A, int begin, int end) {		//问题的划分
	int mid_Index;
	int mid = begin + ((end - begin) >> 1);
	if ((A[begin] >= A[mid] && A[begin] <= A[end])||(A[begin] >= A[end] && A[begin]<=A[mid]))
	{
		mid_Index = begin;
	}
	else if ((A[end] >= A[mid] && A[end] <= A[begin]) || (A[end] >= A[begin] && A[end] <= A[mid]))
	{
		mid_Index = end;
	}
	else {
		mid_Index = mid;
	}
	swap(A[mid_Index], A[begin]);

这是前半段的,后面的就是Partition里面的内容,一个字符都没变的呦~,思路就是找出介于中间的值,这样有一个大致的参考,不确定性为%33,由原来的%100下降到%33,时间上肯定快了不少.

***归并排序:***分治模式
分解:将N个元素分成各含n/2个元素的子序列
解决:对两个子序列递归地排序
合并:合并两个已排序的子序列以得到排序结果
与快排的不同:
1.归并的划分比较随意
2.重点是合并
合并原理:两个数组,每次取它的第一个元素,相互比,较小的元素,实现合并是比较简单的

#include<cstdio>
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
#include<math.h>
#include<stdlib.h>
using namespace std;
vector<int> S = { 1,2,34,543,4,234,15513,242,25343,43434 };
void merge(vector<int>& A, int low, int mid, int high);
void merge_sort(vector<int>& A, int low, int high)
{
	if (low < high) {
		int mid = low + ((high - low) >> 1);
		merge_sort(A, low, mid);		//注意到快排的中间是不算进去的
		merge_sort(A, mid + 1, high);	//但是归并排序是算进去的
		merge(A, low, mid, high);
	}
}

vector<int>& helper(S);
void merge(vector<int>& A, int low, int mid, int high)
{//开辟辅助空间,把辅助空间作为队伍,把原数组作为目标空间,往回拷贝,只开辟一次空间就好,取它的最大元素个数
	int k = 0;		//计总数,填入原来位置
	int i = 0;
	int j = 0;
	while (low+i != mid && mid + 1+j != high) {		//左边没有走完,同时,右边也没有走完,则继续走
		if (helper[low + i] >= helper[mid + 1 + j]) {
			A[k++] = helper[mid + 1 + j];
			j++;
		}
		else if (helper[low + i] < helper[mid + 1 + j]) {
			A[k++] = helper[low + i];
			i++;
		}
	}
	if (low == mid) {
		while (mid + 1 != high) {
			A[k++] = helper[mid + 1 + j];
			j++;
		}
	}
	else if (mid + 1 == high) {
		while (low != high) {
			A[k++] = helper[low + i];
			++i;
		}
	}
}

int main()
{
	merge_sort(S, 0, S.size() - 1);
	for (vector<int>::iterator a = S.begin(); a != S.end(); ++a) {
		cout << *a << " ";
	}
	return 0;
}

算法题:五种常见方法
1.举例法,找出其中蕴藏的某种规律
2.模式匹配法(见多识广):看看现有问题和已知的基础算法之间有没有每种关系或者说变化
3.简化推广法:修改约束条件,比如数据类型或数据量,我们先来实现简化后的问题,求出解法后推出更加一般的算法
4.简单构造法:从n=1的结果入手,依次解决n=2,n=3…乃至到一般情况
5.数据结构头脑风暴:所以数据结构走一遭

算法案例:调整数组顺序使得奇数位于偶数前面:输入一个整数数组,调整数组中的数字顺序,使得所有奇数位于数组的前半部分,偶数位于后半部分,要求时间复杂度为O(n)
解法:双指针,交换

void  f(vector<int>& A, int left, int right)
{
	int i = left, j = right - 1;
	while (i <= j) {
		while ((A[i] & 1) != 0&&i<=j) {		//判断奇数偶数
			++i;
		}
		while ((A[j] & 1) == 0&&i<=j) {
			--j;
		}
		swap(A[i], A[j]);
	}
}

算法案例:超过一半的元素:数组中有一个数字出现的次数超过了数组长度的一般,找出这个数字
可以使用"第K个元素"里顺序统计的思想,求解第N/2下标的元素

int SelectK(vector<int>&A,int p,int r,int k)
{//此时传入的是A.size()/2
	int q = Partition(A, p, k);
	int qK = q - p + 1;		//主元是第几个元素
	if (qK == k)	return A[q];
	else if (qK > k)	return SelectK(A, p, q - 1,k);
	else {
		return SelectK(A, q + 1, r, k-qK);		//注意这里不是求第K个元素了,而是求第k-qK个元素
	}											//因为左边已经被舍弃了,需要从右边来找第k-qK个元素
		 
}

最小可用ID:非负数组(乱序)中找出最小的可分配的id(从1开始编号),数据量为1000000
题意:最小的缺席的那个数
比如数组1342576910…(大于10),我们可以说最小可用id为8
法一:创建新的辅助空间,依次填入原数组数据,两次遍历数组,O(n)的时间复杂度,但是空间复杂度比较高(申请1000000个int空间,想想就觉得头大)
法二:不断的partition划分,每次划分就会缩小一半判断当前所指的元素array[index]与yindex的大小,如果大了,表示左边有空缺,如果相等,则表示右边有空缺,再递归调用函数即可,边界情况单独考虑.

第K个元素:以尽量高的效率求出一个乱序数组中按数值顺序排序的第k个元素值
核心思想:

int SelectK(vector<int>&A,int p,int r,int k)
{
	int q = Partition(A, p, k);
	int qK = q - p + 1;		//主元是第几个元素
	if (qK == k)	return A[q];
	else if (qK > k)	return SelectK(A, p, q - 1,k);
	else {
		return SelectK(A, q + 1, r, k-qK);		//注意这里不是求第K个元素了,而是求第k-qK个元素
	}											//因为左边已经被舍弃了,需要从右边来找第k-qK个元素
		 
}

题目描述
寻找发帖水王:思想:消除法,相邻两个元素,以前面的元素为基准,计数count,相同则count++,不相同则减减,最后选择的candidates就是待选元素.

/当水王发帖数量大于一半时才可以这样写
void print(vector<int>& array)
{
	int candidates = array[0];
	int count = 1;
	for (int i = 1; i < array.size(); ++i)		//对于不相同的两个元素,直接消除,因为个数大于一半,所以剩下的元素candidates水王
	{
		if (count == 0) {
			candidates = array[i];
			count++;
		}
		if (array[i] == candidates) {
			count++;
		}
		else {
			count++;
		}
	}
	count << candidates;
}

但是,当水王发的帖子数只有总数的一半时,我们会发现,消到最后时,总是最后一个或者倒数第二个(此时的倒数第二个就是candidates)为我们要寻找的水王,所以不失一般性,我们可以每次选array[i]时都和最后一个元素作比较,单独计数,当最后的计数为N/2时,就表示最后一个元素就是水王,否则,当前candidates就是水王
比如初始数组
aaaaa12345
两两消除,最后只剩下cnadidates=a;
最后一个元素为5,我们每次选取当前元素和最后一个元素比较,计数不足N/2,则可以确定当前candidates就是水王
再比如初始数组
5a2aa34a6a
两两消除,最后只剩下6和a,此时6是candidates,每个元素的比较使得a的count_last==N/2,也就确定最后一个元素就是水王

讲两道关于逆序对的题目:
1.合并有序数组:给定两个排序后的数组A,B,假设A后面有足够的空间容纳B,求合并后的数组

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;

void merge(vector<int>& A, vector<int>& B)
{
	int i = 0;
	vector<int> C;
	vector<int>::iterator a = A.begin();
	vector<int>::iterator b = B.begin();
	while( a != A.end() && b != B.end())
	{
		if ((*a) <= (*b)) {
			C[i++] = *a;
			a++;
		}
		else {
			C[i++] = *b;
			++b;
		}
	}
	if (a != A.end()) {
		while(a != A.end()) {
			C[i++] = *a;
			++a;
		}
	}
	else if (b != B.end()) {
		while (b != B.end()) {
			C[i++] = *b;
			++b;
		}
	}
	for (int i = 0; i < C.size(); ++i) {
		cout << C[i] << " ";
	}
}

int main() {
	vector<int> A = { 1,3,5,7,9 };
	vector<int> B = { 2,4,6,8,10 };
	merge(A, B);
	return 0;
}

2.逆序对个数:一个数组,如果左边的数大,右边的数小,则称这两个数为一个逆序对,求数组中有多少个逆序对?

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
vector<int> C;
int nixv = 0;
void merge(vector<int>& A,int l,int mid,int r)
{
	vector<int> helper(A);
	int left = l, right = mid+1;
	int current = l;
	while (left <= mid && right <= r) {
		if (A[left] <= A[right]) {
			helper[current++] = A[left++];
		}
		else {
			helper[current++] = A[right++];
			nixv += mid - left + 1;
		}
	}
	while (left <= mid) {
		helper[current] = A[left];
		current++;
		left++;
	}//从前到后再次合并为一个有序的数组

}

```cpp
扩充:如何生成一系列随机数范围为[a,b]?
#include<iostream>//里面有rand()函数,srand()种子,两者都没有返回值,但是rand()不接受参数,srand接收参数(因为要使得种子不同)
#include<time.h>			//里面有time()函数,可以做强制类型转换
#define random(x) (rand()%x)		//这里注意rand()函数是不接受参数的

int main()
{//只要种的种子不一样,得到的随机数也就不一样,
	int a,b;
	cin>>a>>b;
	srand((int)time(0));
	for(int i=0;i<10;++i)
	{
		cout<<random(a-b)+a<<" ";
	}	
	return 0;
}
/*也就是两步走,1.种种子:srand(time(0))  2.输出rand(a
-b)+a*/
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shallow_Carl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值