偶尔会翻翻之前的博客,对之前写的进行补充修改,可能会有点乱,见谅哈~
快速排序
以左端点为基准
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;
}