快排+归并(补充了求逆序对)+堆排序+拓扑序列


偶尔会翻翻之前的博客,对之前写的进行补充修改,可能会有点乱,见谅哈~

快速排序

以左端点为基准

void quicksort(int arr[], int beg, int end)
{
	if (beg >= end) return;
	int key = arr[beg], l = beg - 1, r = end + 1;
	while (l < r)
	{
		do l++; while (arr[l] < key);
		do r--; while (arr[r] > key);
		if (l < r) swap(arr[l], arr[r]);
	}
	quicksort(arr, beg, r);//***
	quicksort(arr, r+1, end);
}

比如这个样例int arr[] = { 3,6,4,7,10,3,6,8,9,11,22,0,-3,-4 };,
quicksort(arr, beg, l-1);quicksort(arr, l, end);会陷入(0,4)的死循环

我们模拟一遍:
第一遍while循环后arr数组变成 {-4, -3, 0, 3, 10, 7, 6, 8, 9, 11, 22, 4, 6, 3}
接着进入quicksort(0,3)
然后此时key=-4,从第一个开始走,跳出的时候l=0,r=0
然后进入下一次递归quicksort(arr, 0,-1,);quicksort(arr, 0, 3);
因为if (beg >= end) return;这一步递归结束,开始quicksort(arr, 0, 3);
这下和上一次递归一模一样直接死循环了。

分析一下原因
do l++; while (arr[l] < key);跳出的时候arr[l]一定大于等于key,
do r--; while (arr[r] > key);跳出的时候arr[r]一定小于等于key,都会有等于的情况。
但是注意,此时比较的是左端点arr[beg],l最小值是0(跳出时l=beg),如果像上面那个样例那样,当前区间从第1个数一直到最后都比第0个数大(arr[i=beg+1~end]>key,跳出时r=beg)(do-while(l)do-while®各来一遍直接跳出外层大while循环),如果像右端点那样quicksort(arr, beg, l-1);quicksort(arr, l, end);下一步循环为quicksort(arr, 0, -1);quicksort(arr, 0, end);

但如果是quicksort(arr, beg, r);quicksort(arr, r+1, end);就不会陷入(0,end)的死循环。
上面那个样例跳出while时r=beg,那么就是quicksort(arr, 0, beg);quicksort(arr, beg+1, end);不会出现0-n这种情况。

右端点用(l-1,r)和上面的分析思路一样,就不写了。下面(边界问题–防止无限划分)有个链接,链接里面分析的时右端点作为key。

以右端点为基准

void quicksort(int arr[], int beg, int end)
{
	if (beg >= end) return;
	int key = arr[end], l = beg - 1, r = end + 1;
	while (l < r)
	{
		do l++; while (arr[l] < key);
		do r--; while (arr[r] > key);
		if (l < r) swap(arr[l], arr[r]);
	}
	quicksort(arr, beg, l-1);//***
	quicksort(arr, l, end);
}

以中间值为基准

void quicksort(int arr[], int beg, int end)
{
	if (beg >= end) return;
	int mid = (beg + end) / 2;
	int key = arr[mid], l = beg - 1, r = end + 1;
	while (l < r)
	{
		do l++; while (arr[l] < key);
		do r--; while (arr[r] > key);
		if (l < r) swap(arr[l], arr[r]);
	}
	quicksort(arr, beg, r);
	quicksort(arr, r+1, end);
}

为什么采用do while而不是while呢:
在这里插入图片描述

边界问题–防止无限划分

快排属于分治算法,最怕的就是 n分成0和n,或 n分成n和0,这会造成无限划分

关于边界取值问题总结一下,如果以左端点为基准,分界点是取右指针,如果是右端点为基准,分界点取左指针,解释见下面:
AcWing 785. 快速排序算法的证明与边界分析(题解)

关于无限划分这块我举个例子(因为现在还不少特别清楚,暂时先记一下,等以后彻底搞明白再修改)->(做右端点的边界问题现在应该是搞明白了)。

下面这一小段是之前写的关于边界问题-取中间点做基值,上面的是左端点边界问题的样例分析
以中间值为基准,取l指针为分界
(这个代码是有问题的,下面那个特判后就没问题了)

void quicksort(int arr[], int beg, int end)
{
	if (beg >= end) return;
	int mid = (beg + end) / 2;
	int key = arr[mid], l = beg - 1, r = end + 1;
	while (l < r)
	{
		do l++; while (arr[l] < key);
		do r--; while (arr[r] > key);
		if (l < r) swap(arr[l], arr[r]);
	}
	quicksort(arr, beg, l-1);
	quicksort(arr, l, end);
}

比如数据:

5
1 4 3 2 5

l开始指向-1,r开始指向5,key=arr[2]=3;
开始移动,移动到l指向1(arr[1]=4),r指向3(arr[3]=2),停止交换
变成:1 2 3 4 5 l<r继续移动,移动到l指向2(arr[2]=3),r移动到2(arr[2]=3) 停止,l==r,不交换,跳出while循环
此时如果是以l为分界,就是(0,1)(2,4)
就是(1,2)一组,对于这一组继续快排
l=-1,r=2,key=arr[0]=1,接着移动,l指向0,r指向0停止,跳出while循环,下一轮快排(0,-1)(0,1) (0,1)这一组递归快排就无限循环,这就是无限划分(应该是吧,希望有大佬可以提点一下

可以这样改善一下:

void quicksort(int arr[], int beg, int end)
{
	if (beg >= end) return;
	int mid = (beg + end) / 2;
	int key = arr[mid], l = beg - 1, r = end + 1;
	while (l < r)
	{
		do l++; while (arr[l] < key);
		do r--; while (arr[r] > key);
		if (l < r) swap(arr[l], arr[r]);
	}
	quicksort(arr, beg, l-1);
	if(l!=beg) quicksort(arr, l, end);
}

分界这块如果现场推对我来说实在有点困难,还是记结论吧,取左基准找右边界,取右基准找左边界,取中间基准找有边界
学习不易,猫猫叹气~

归并排序

关于归并排序是怎么排的请看链接的图解
关键代码

void mergesort(int beg, int end)
{
	if (beg >= end) return;
	int mid = beg + end >> 1;//取中点
	mergesort(beg, mid); mergesort(mid + 1, end);
	//先一直二分,分到只有一个元素的时候beg==end,返回上一级,这时候有两个元素
	//(beg,mid)区间的,对两个元素进行排序,排序完递归(mid+1,end)区间的,
	//对该区间(可能是两个元素可能一个元素)进行排序,再返回上一级,对两个区间合并成的大区间进行排序
	int k = 0;
	int i = beg, j = mid + 1;
	//一个大区间分成两个小区间,通过对两区间的首元素对比放入新数据进行对大区间的排序
	while (i <= mid && j <= end)
	{
		if (arr[i] <= arr[j]) temp[k++] = arr[i++];
		else temp[k++] = arr[j++];
	}
	while (i <= mid) temp[k++] = arr[i++];//一个区间还有省的就全放入temp数组
	while (j <= end) temp[k++] = arr[j++];
	for (int i = beg, q = 0; i <= end; i++, q++)
		arr[i] = temp[q];
		//关于这个的q=0,是每次对一个大区间排序后的元素防止位置,再把这个排序后的位置放入原数组的相应位置
}

完整代码

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1000000;
int arr[N],temp[N];
void mergesort(int beg, int end);
int main()
{
	int n;
	cin >> n;
	for (int i = 0; i < n; i++) 
		cin >> arr[i];
	mergesort(0, n - 1);
	for (int i = 0; i < n; i++)
		cout << arr[i]<<" ";
	return 0;
}
void mergesort(int beg, int end)
{
	if (beg >= end) return;
	int mid = beg + end >> 1;
	mergesort(beg, mid); mergesort(mid + 1, end);
	int k = 0;
	int i = beg, j = mid + 1;
	while (i <= mid && j <= end)
	{
		if (arr[i] <= arr[j]) temp[k++] = arr[i++];
		else temp[k++] = arr[j++];
	}
	while (i <= mid) temp[k++] = arr[i++];
	while (j <= end) temp[k++] = arr[j++];
	for (int i = beg, q = 0; i <= end; i++, q++)
		arr[i] = temp[q];
}

补充:归并排序求逆序对(21.7.26)

对于这个新添加的东西进行解释:
如果arr[j]<arr[i],满足i<j,arr[j]<arr[i].根据归并的原理,当前分治段左区间arr[i]后面的数都比arr[i]大,(arr[i],arr[j])能构成逆序对,那么arr[i]后面的数与arr[j]也能构成逆序对,mid-i+1表示的是当前左区间剩余数的个数(包括当前正在比较的这个数),这些所有的数与右区间正在比较的数都可以构成逆序对。

cnt += (mid - i + 1);//只有这里有变化
void mergesort(int beg, int end)
{
	if (beg >= end) return;
	int mid = beg + end >> 1;
	mergesort(beg, mid); mergesort(mid + 1, end);
	int i = beg, j = mid + 1;
	int k = 0;
	while (i <= mid && j <= end)
	{
		if (arr[i] <= arr[j]) temp[k++] = arr[i++];
		else {
			temp[k++] = arr[j++];
			cnt += (mid - i + 1);//只有这里有变化
		}
	}
	while (i <= mid) temp[k++] = arr[i++];
	while (j <= end) temp[k++] = arr[j++];
	for (int i = beg, p = 0; i <= end; i++, p++)
		arr[i] = temp[p];
}

堆排序

题目链接–堆排序
今天是2021年7月23号,距离我上一次整理堆排序一个月左右的时间,也不知道今天脑子哪里有个坑,竟然连堆排序都忘了,返回去看之前的博客竟然看不懂,emmm注释写的太少了,很多细节点和原理没有注明,所有今天重新写一份并补充个人理解,希望不要再忘了。
建立小根堆:

void BuildHeap(int n)
{
	for (int i = n; i >= 1; i--)
	{
		JustifyHeap(i, n);
		//这里以i为根节点的树进行排序交换使其成为一个小根堆
		//接着在沿着根往上,把包含整个小根堆的大一些的树再次构建成一个小根堆,逐级向上。
		//举个例子:已经构造好两个小根堆,扩大树的范围,找到的第一个树就是包含这两个
		//小根堆已经树的根节点的树,再次构造小根堆只要对大树的根节点进行修改,使之符合小根堆的构造就行
	}
}
#include<iostream>
using namespace std;
const int N = 1e5 + 5;
int occ[N];
void BuildHeap(int n);
void JustifyHeap(int root, int n);
int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> occ[i];
	BuildHeap(n);
	//先是建立小根堆
	while (m--)
	{
		cout << occ[1]<<" ";
		swap(occ[1], occ[n--]);
		//每一次输出第一个(根节点)后将末尾元素与之交换再从新对小根堆进行更新
		//如果只是要去前m个数据(m<<n),堆排序是一个很好的办法
		JustifyHeap(1, n);
	}
	return 0;
}
void BuildHeap(int n)
{
	for (int i = n; i >= 1; i--)
	{
		JustifyHeap(i, n);
	}
}
void JustifyHeap(int root, int n)
{
	int p = root, q = 2 * p;
	while (q <= n)
	{
		if (q + 1 <= n && occ[q + 1] < occ[q]) q++;
		if (occ[q] < occ[p]) swap(occ[p], occ[q]);
		p = q;
		q = 2 * p;
	}
}

拓扑序列

题目链接–有向图的拓扑序列
只有有向图存在拓扑序列,且有向图不构成环
在这里插入图片描述

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e5 + 5;
int e[N], ne[N], h[N], idx, que[N], n;
int Head = 0, Tail = -1, rd[N];//队头出队,队尾入队,数组记录入度
void add(int x, int y)
{
	e[idx] = x, ne[idx] = h[y], h[y] = idx++;
}
bool topsort()
{
	for (int i = 1; i <= n; i++)
		if (rd[i] == 0) {
			que[++Tail] = i;
		}
	while (Head <= Tail)
	{
		for (int i = h[que[Head++]]; i != -1; i = ne[i])
		{
			rd[e[i]]--;
			if (rd[e[i]]==0) que[++Tail] = e[i];
			//我一开始还开了个数组vis来按段该节点是否被访问过,防止重边的情况,但后
			//来发现,判断条件的rd[e[i]]==0就已经确保了该节点第一次被访问,即使重边,经过rd[e[i]]--,入度便小于0了,所以不用担心重边造成的多次访问
		}
	}
	return Tail == n - 1;//如果有向图的所有节点都在序列里面就说明无环,否则说明有环,无法产生拓扑序列
}
int main()
{
	int m, x, y;
	memset(h, -1, sizeof h);
	cin >> n >> m;
	while (m--)
	{
		cin >> x >> y;//题目要求x指向y
		add(y, x);
		//我定义的add函数add(int x,int y)只能通过y找到x,也就是y指向x
		//而后期在topsort函数里我通过x的出度对对应节点的入度进行修改,所以这里把x,y顺序倒一下
		//或者把add函数修改一下e[idx] = y, ne[idx] = h[x], h[x] = idx++;这里就是add(x,y);
		rd[y]++;
	}
	if (topsort())
	{
		for (int i = 0; i <= Tail; i++)
			cout << que[i] << " ";
	}
	else cout << -1;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值