Chapter 09 Internal sorting
- Definition
- Stable and Unstable
Insertion based sorting
Straight inserting based sorting
-
从小到达排序,很简单的思想,对于已经排序好的序列 a i − a j , 0 < = i < = j a_i-a_j,0<=i<=j ai−aj,0<=i<=j,对于元素 a j + 1 a_{j+1} aj+1,假设需要插入到第 k k k个位置,那么从 a k , i − 1 < = k < = j + 1 a_k,i-1<=k<=j+1 ak,i−1<=k<=j+1开始的向后的所有的元素都要后移。
-
时间复杂度分析,排序长度为 n n n的序列第 i , 1 < = i < n i,1<=i<n i,1<=i<n个元素需要的比较的次数最坏为 i i i次,总共 ∑ i = 1 n − 1 i = n ( n − 1 ) 2 \sum_{i=1}^{n-1}i=\frac{n(n-1)}{2} ∑i=1n−1i=2n(n−1)次,需要移动的元素最多为 i i i个,总共 ∑ i = 1 n − 1 i = n ( n − 1 ) 2 ( 没 算 移 动 a i 的 那 个 元 素 ) \sum_{i=1}^{n-1}i=\frac{n(n-1)}{2}(没算移动a_i的那个元素) ∑i=1n−1i=2n(n−1)(没算移动ai的那个元素)。
-
时间复杂度 O ( n 2 ) O(n^2) O(n2)
-
这个排序是Stable的。
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int maxn = 10005;
/*
struct Node{
int val;
};
struct List{
Noode node[maxn];
int len=0;
};
*/
vector<int>llist;
void insertionBasedSorting(vector<int>& llist) {
int n = llist.size();
// 第一个元素已经有序,排序剩下的元素
for (int i = 1; i < n; i++) {
int tmp = llist[i];// 获取当前需要插入的元素
int pos = i - 1;// 当前需要插入的元素前面的一个元素
while (pos >= 0 && llist[pos] > tmp) {// 我们需要找到第一个llist[pos]<tmp的pos
llist[pos + 1] = llist[pos];
pos--;
}
// llist[i]这个元素最小,那么应该放在llist[pos]这个位置上
if (pos == 0 && llist[pos] > tmp) { // 不写等于号使其成为稳定的排序
llist[pos + 1] = llist[pos];
llist[pos] = tmp;
}
// 这个元素不是最小的,应该放在pos+1的位置上
else {
llist[pos + 1] = tmp;
}
}
}
int main() {
int n;
cin >> n;
int iin;
for (int i = 0; i < n; i++) {
scanf("%d", &iin);
llist.push_back(iin);
}
insertionBasedSorting(llist);
for (int i = 0; i < llist.size(); i++) {
cout << llist[i] << " ";
}
cout << endl;
return 0;
}
// 5 4 3 2 5 6
Dichotomy inserting based sorting
-
对比直接插入排序,查找不再一个一个比较,采用二分的方法去寻找。
-
二分查找减少的比较的时间破坏了稳定性。
-
二分查找的过程需要注意一些细节。
-
定义函数 i n t b i n a r y S e a r c h ( v e c t o r < i n t > & v , i n t b e g i n _ p o s , i n t e n d _ p o s , i n t v a l ) int binarySearch(vector<int>\&v,int~begin\_pos,int ~end\_pos,int~val) intbinarySearch(vector<int>&v,int begin_pos,int end_pos,int val)
函数实现:
当val比 a [ b e g i n _ p o s ] a[begin\_pos] a[begin_pos]海小的时候返回 b e g i n _ p o s − 1 begin\_pos-1 begin_pos−1
当val比 a [ e n d _ p o s ] a[end\_pos] a[end_pos]还大的时候返回 e n d _ p o s + 1 end\_pos+1 end_pos+1
当val比 a [ ] a[] a[]中的某个元素大的时候,返回这个位置。
总结起来就是这一段代码:很好记忆的是吧?
// 找到大于等于val的第一个元素的位置,如果没有大于val的元素,返回的是begin_pos-1 int binarySearch(vector<int>& llist, int begin_pos, int end_pos, int val) { int left = begin_pos, right = end_pos; int mid; while (left<=right) { mid = (left + right) / 2;// (1,3)->2得到的中间的数字,(1,4)组合,那么得到(1+4)/2=2,得到一个偏近1的位置。。。 (a,b)代表left和right的位置分别为(a,b) // 特殊的,(1,1)->1, (1,2)->1 if (llist[mid] == val) { return mid; } else if (llist[mid] < val) { left = mid + 1; } else { right = mid - 1; } } // 这个时候我们如何找到这个>=val的pos? // 跳出循环的条件只有left>right // 由于mid<=right,所以left最多只能大right 1个单位,也就是跳出循环的条件是right=left+1 // 那么问题来了 // 1. 假设之前通过else if(llist[mid]<val)增加left,left==(根据假设得到)mid+1==(通过left>right的跳出条件得到)right+1,也即是mid==right,那么a[mid]<=val,不是等于, // 那么必然是a[mid]==a[right]<val,这个时候肯定是返回left==mid+1==right+1 // 2. 假设之前通过else if(llist[mid]<val)减小right, right==(根据假设得到)mid-1,left==right+1(通过left>right的跳出条件得到),那么left=mid,也就是要么(1,1)这样的,要么(1,2)这样的, // 那么这个时候,a[mid]==a[left]>val,val是最小的元素,我们需要返回的是right,这是插入val的位置 // 那么如果right<begin_pos,我们需要返回的是right,因为这个时候所有的元素都比val要大,我们直接返回right // 如果right>=0,说明>val的第一个元素是left==mid // 下面的代码很好记是吧? if (right < begin_pos) { return right; } if (left > end_pos) { return left; } return mid; }
-
代码
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int maxn = 10005;
/*
struct Node{
int val;
};
struct List{
Noode node[maxn];
int len=0;
};
*/
vector<int>llist;
// 找到大于等于val的第一个元素的位置,如果没有大于val的元素,返回的是begin_pos-1
int binarySearch(vector<int>& llist, int begin_pos, int end_pos, int val) {
int left = begin_pos, right = end_pos;
int mid;
while (left<=right) {
mid = (left + right) / 2;// (1,3)->2得到的中间的数字,(1,4)组合,那么得到(1+4)/2=2,得到一个偏近1的位置。。。 (a,b)代表left和right的位置分别为(a,b)
// 特殊的,(1,1)->1, (1,2)->1
if (llist[mid] == val) {
return mid;
}
else if (llist[mid] < val) {
left = mid + 1;
}
else {
right = mid - 1;
}
}
// 这个时候我们如何找到这个>=val的pos?
// 跳出循环的条件只有left>right
// 由于mid<=right,所以left最多只能大right 1个单位,也就是跳出循环的条件是right=left+1
// 那么问题来了
// 1. 假设之前通过else if(llist[mid]<val)增加left,left==(根据假设得到)mid+1==(通过left>right的跳出条件得到)right+1,也即是mid==right,那么a[mid]<=val,不是等于,
// 那么必然是a[mid]==a[right]<val,这个时候肯定是返回left==mid+1==right+1
// 2. 假设之前通过else if(llist[mid]<val)减小right, right==(根据假设得到)mid-1,left==right+1(通过left>right的跳出条件得到),那么left=mid,也就是要么(1,1)这样的,要么(1,2)这样的,
// 那么这个时候,a[mid]==a[left]>val,val是最小的元素,我们需要返回的是right,这是插入val的位置
// 那么如果right<begin_pos,我们需要返回的是right,因为这个时候所有的元素都比val要大,我们直接返回right
// 如果right>=0,说明>val的第一个元素是left==mid
// 下面的代码很好记是吧?
if (right < begin_pos) {
return right;
}
if (left > end_pos) {
return left;
}
return mid;
}
void insertionBasedSorting(vector<int>& llist) {
int n = llist.size();
// 第一个元素已经有序,排序剩下的元素
for (int i = 1; i < n; i++) {
int tmp = llist[i];// 获取当前需要插入的元素
int pos = i - 1;// 当前需要插入的元素前面的一个元素
int pos_insert = binarySearch(llist, 0, i - 1,tmp);
if (pos_insert < 0) {
for (int j = i - 1; j > pos_insert; j--) {
llist[j + 1] = llist[j];
}
llist[0] = tmp;
}
else {
for (int j = i - 1; j >= pos_insert; j--) {
llist[j + 1] = llist[j];
}
llist[pos_insert] = tmp;
}
}
}
int main() {
int n;
cin >> n;
int iin;
for (int i = 0; i < n; i++) {
scanf("%d", &iin);
llist.push_back(iin);
}
insertionBasedSorting(llist);
for (int i = 0; i < llist.size(); i++) {
cout << llist[i] << " ";
}
cout << endl;
return 0;
}
// 6 4 3 2 5 6 6
Two way inserting based sorting
- 设待排序数组为 a [ ] a[~] a[ ],设置一个和 a [ ] a[~] a[ ]数组相同长度的数组 b [ ] b[~] b[ ],令 b [ 0 ] = a [ 0 ] b[0]=a[0] b[0]=a[0],然后从 a [ 1 ] − a [ n − 1 ] a[1]-a[n-1] a[1]−a[n−1]开始排序,得到的元素插在 b [ 0 ] b[0] b[0]的前面或者后面(从小到大排序的话小的元素插在b[0]的前面),这样的一个排序的方法。
- 当 a [ 0 ] a[0] a[0]是待排序数组中最大的或者是最小的元素的时候,退化为直接插入排序。
- 可以减少元素的移动的次数,比较的次数不变,记录移动的次数大概为 n 2 8 \frac{n^2}{8} 8n2,需要 n n n个辅助的存储空间。
- 具体的实现:使用两个指针 f i r s t first first和 l a s t last last指示 d [ ] d[~] d[ ]中的第一个和最后一个元素,每次的插入排序是在 f i r s t first first~ l a s t last last的范围内进行的。
- Stable。
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
// first和last可能为负数,也即是last和first始终保持着last>=first的关系。
// 那么first和last指示的元素在d[]中的实际位置为(first+n)%n和(last+n)%n
int insert(vector<int>& v, vector<int>& d, int& first, int& last, int& val) {
// val>=d[last]终止
int n = v.size();
int cur_pos_last = (last + n) % n;
int cur_pos_first = (first + n) % n;
int tmp_last = last;
// 当val比所有的元素要小
if (d[cur_pos_first] > val) {
first--;
return first;
}
// val并不比所有的元素要小
while (d[cur_pos_last] > val) {
// 移动元素
d[(cur_pos_last + 1)%n] = d[cur_pos_last];
tmp_last--;
cur_pos_last = (tmp_last + n) % n;
}
// 说明当前满足d[last]<=val,那么插入位置就是last+1。
last++;
return tmp_last + 1;
}
void twoWayInsertionBasedSorting(vector<int>& v) {
int n = v.size();
vector<int>d(n);
if (n == 0) {
return;
}
d[0] = v[0];
int first = 0, last = 0;
for (int i = 1; i < n; i++) {
int val = v[i];
int insert_pos = (insert(v, d, first, last, val) + n) % n;
d[insert_pos] = val;
}
for (int i = 0; i < n; i++) {
v[i] = d[(first + n) % n];
first++;
}
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
twoWayInsertionBasedSorting(a);
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
}
Shell sorting
- 缩小增量排序,直接插入排序中当元素已经有序的时候需要进行 n n n次比较即可,时间复杂度为 O ( n ) O(n) O(n),这个时候直接插入排序的效率比较高。直接插入排序在** n n n值较小的时候插入的效率也高**(移动元素的次数少),这样的话我们结合这两个特征就可以得到Shell排序的方法。
- 首先将待排记录分成若干个子序列,然后对这若干个小的序列进行插入排序,最后再对整个序列进行一次插入排序。
- Unstable.
- 平均时间复杂度大约为 O ( n 1.3 ) O(n^{1.3}) O(n1.3),最坏 O ( n 2 ) O(n^2) O(n2)。
- 例子:
- 实现起来的话也不难,设置一个步长变量 s = n 2 s=\frac{n}{2} s=2n,变化规则如下 s , s / 2 , s / 2 2 . . . s,s/2,s/2^2... s,s/2,s/22...,对于第 i i i个元素和第 i + s i+s i+s个元素,排序这两个元素,然后 s > > = 1 s>>=1 s>>=1,不断的重复这个过程,最后来一次插入排序。
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int>llist;
void insertionBasedSorting(vector<int>& llist) {
int n = llist.size();
// 第一个元素已经有序,排序剩下的元素
for (int i = 1; i < n; i++) {
int tmp = llist[i];// 获取当前需要插入的元素
int pos = i - 1;// 当前需要插入的元素前面的一个元素
while (pos >= 0 && llist[pos] > tmp) {// 我们需要找到第一个llist[pos]<tmp的pos
llist[pos + 1] = llist[pos];
pos--;
}
// llist[i]这个元素最小,那么应该放在llist[pos]这个位置上
if (pos == 0 && llist[pos] > tmp) { // 不写等于号使其成为稳定的排序
llist[pos + 1] = llist[pos];
llist[pos] = tmp;
}
// 这个元素不是最小的,应该放在pos+1的位置上
else {
llist[pos + 1] = tmp;
}
}
}
void shellBasedSorting(vector<int>& v) {
int n = v.size();
if (n <= 1) {
return;
}
int s = n / 2;
while (s >= 1) {
for (int i = 0; i + s < n; i++) {
if (v[i + s] < v[i]) {
swap(v[i], v[i + s]);
}
}
s >>= 1;
}
insertionBasedSorting(v);
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
shellBasedSorting(a);
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
}
Swap based sorting
交换排序,基本的思想,两两比较需要排序的数据,需要交换的就交换,直到所有的数据都排列好。
Bubble sorting
-
每次从前往后扫描所有的元素,找到最大的元素,不断地向后推,循环n次,这样得到的最终的序列就是已经排序号的序列。
-
时间复杂度最坏的情况逆序的情况, O ( n 2 ) O(n^2) O(n2), n − 1 n-1 n−1次比较和元素的交换。
-
稳定的算法。
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
void bubbleSorting(vector<int>& v) {
int n = v.size();
// 第一次把最大的冒到最后,其次次大...
for (int i = 0; i < n; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (v[j] > v[j + 1]) {
swap(v[j], v[j + 1]);
}
}
}
return;
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
bubbleSorting(a);
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
}
Quick sorting
-
通过选出的基准将序列分为两个部分,小的一部分在一边,大的部分在一遍,然后对这两个部分重复上面的过程。
-
附设两个指针low和high,它们的初值分别是一个序列的第一个和最后一个记录的位置,设枢轴记录的关键字为pivotKey。
首先从high所指位置起向前搜索直到第一个关键字小于pivotKey的记录和枢轴记录交换,然后从low所指位置起向后搜索,找到第一个关键字大于pivotKey的记录和枢轴记录互相交换,重复交替这两步直到low=high为止。
-
快速排序的平均时间复杂度经过研究为 T a v g ( n ) = k n l n ( n ) T_{avg}(n)=kn~ln(n) Tavg(n)=kn ln(n),k是一个常数。经过研究,快速排序的常数因子最小,平均性能最好,如果初始序列基本有序或者是基本无序,变成冒泡排序。
因此,平均时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
空间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)主要是栈的开销。
-
改进:三者取中,选择合适的基准程序改善快速排序的性能。
-
不稳定Unstable.
-
快速排序算法的优化:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1BD81XGE-1615953446768)(第九章-内部排序/image-20210314163102062.png)]
如何进行优化?
如何进行优化?
- 快速排序代码:
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
void quickSort(vector<int>& v, int left, int right) {
if (left >= right) {
return;
}
int original_left = left, original_right = right;
int base = v[left];
while (left < right) {
// 找到第一个小于base的元素
while (right > left && v[right] >= base) {
right--;
}
// 该元素是第一个小于base的元素
if (right > left) {
v[left] = v[right];
}
// 如果没找到小于base的元素,那么必定有left==right,v[left]这个元素我们已经处理过了,不需要再处理,所以上面我们用left<right而不用left<=right
// 找到第一个大于base的元素
while (left < right && v[left] <= base) {
left++;
}
if (left < right) {
v[right] = v[left];
}
}
// 跳出循环的时候必然有left==right
// 那么
v[left] = base;
quickSort(v, original_left, left - 1);
quickSort(v, left + 1, original_right);
}
void quickSorting(vector<int>& v) {
int n = v.size();
int left = 0, right = n - 1;
quickSort(v, left, right);
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
//bubbleSorting(a);
quickSorting(a);
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
}
Selection based sorting
选择,顾名思义,每次从前往后选择相应的元素放在对应的位置。
Simple selection based sorting
- 不稳定Unstable,因为将元素交换的时候可能出现把原本在前面的元素换到后面的况。
- 时间复杂度 O ( n 2 ) O(n^2) O(n2)
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
void selectionBasedSorting(vector<int>& v) {
int n = v.size();
for (int i = 0; i < n; i++) {
int min_val = v[i], min_pos = i;
for (int j = i + 1; j < n; j++) {
if (min_val > v[j]) {
min_val = v[j];
min_pos = j;
}
}
if (min_pos != i) {
// 不稳定是因为这个交换
swap(v[min_pos], v[i]);
}
}
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
//bubbleSorting(a);
//quickSorting(a);
selectionBasedSorting(a);
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
}
Tree selection based sorting
还没有自己写一遍
- 又称为Tournament Sorting
- 首先在 n n n个数字中进行两两的比较,然后再在 c e i l ( n / 2 ) ceil(n/2) ceil(n/2)的元素中进行两两的比较,重复上述过程,直到找到最大的元素位置,这个过程可以用满二叉树进行描述。
- 每次两两的比较把关键码较小的元素上升到顶部,这个东西叫做胜者树,反之叫做败者树。
- 该二叉树的深度为 c e i l ( n l o g 2 ( n + 1 ) ) ceil(nlog_2(n+1)) ceil(nlog2(n+1)),n为待排序的元素的个数。
- 第一次的比较的次数为 n − 1 n-1 n−1,下面的比较次数均为 l o g 2 ( n ) log_2(n) log2(n)总的关键码的比较的次数为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
- 移动的次数不超过比较的次数。
- 所以时间复杂度为 O ( n l o g 2 ( n ) ) O(nlog_2(n)) O(nlog2(n))
- 空间复杂度 O ( n ) O(n) O(n)。
- Stable的排序算法。
- 胜者树的一个实现如下所示:
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
template <class Type>
class DataNode {
public:
Type data;// 存放数据
int index;// 索引,放在数组中的索引
int active;// 是否参选
};
// 不处理溢出
int powerOfTwo(int n) {
int res = 1;
int pow = 2;
while (n) {
if (n & 1) {
res = res * pow;
}
pow = pow * pow;
n >>= 1;
}
return res;
}
// 更新的时候自下向上进行更新
template <class Type>
void updateTree(Type* tree, int i) {
// 当前的索引是右边的节点
if (i % 2 == 0 && tree[i-1].active==true) {// 对手是左边的节点
tree[(i - 1) / 2] = tree[i - 1];// 更新其父亲节点
}
else if(i%2==1&&tree[i+1].active==true){// 对手是右边的节点
tree[(i - 1) / 2] = tree[i + 1];// 更新其父亲节点
}
else {// 两个节点都是无效的
tree[(i - 1) / 2].active = false;
}
i = (i - 1) / 2;//得到其父亲节点的索引
int j;
while (i) {
if (i % 2 == 0) {//当前节点是右边的节点
j = i - 1;// 获得左边的节点
}
else {
j = i + 1;// 获得右边的节点
}
// 此时,i和j分别指示当前待比较的两个节点
if (!tree[i].active || !tree[j].active) {
if (tree[i].active) {
tree[(i - 1) / 2] = tree[i];
}
else if(tree[j].active){
tree[(i - 1) / 2] = tree[j];
}
else {
tree[(i - 1) / 2].active = false;
}
}
else {
if (tree[i].data < tree[j].data) {
tree[(i - 1) / 2] = tree[i];
}
else {
tree[(i - 1) / 2] = tree[j];
}
}
i = (i - 1) / 2;
}
}
template<class Type>
// n应该是数组长度len满足2^{k-1}<len<=2^k的这个k,也就是k-1<log2(len)<=k,也就是ceil(log2(len))
void tournamentSort(Type a[], int n, int k) {
DataNode<Type>* tree;
DataNode<Type>item;
// 长度为n的序列,建树的时候需要2^n的长度的数组去存储。
int bottomRowSize = powerOfTwo(k);
int treeSize = 2 * bottomRowSize - 1;
int loadindex = bottomRowSize - 1;// 内部节点的个数
tree = new DataNode<Type>[treeSize];
// 下面复制a[]数据到胜者树的叶子节点
for (int i = loadindex; i < treeSize; i++) {
if (i - loadindex < n) {
tree[i].index = i;
tree[i].data = a[i - loadindex];
tree[i].active = true;
}
else {
tree[i].active = false;
}
}
// 下面进行初始化的比较选择较小的项填入到我们的胜者树之中
int i = loadindex, j;// i指示当前正在比较的那一排的第一个元素,j指示当前正在比较的两个元素的第一个
while (i) {
j = i;
while (j < 2 * i) {// 遍历那一层所有的元素
if (!tree[j + 1].active || tree[j].data < tree[j + 1].data) {
tree[(j - 1) >> 1] = tree[j];
}
else {
tree[(j - 1) >> 1] = tree[j + 1];
}
j += 2;
}// 至此,下面的那一排比较完毕
i = (i - 1) >> 1;// 开始比较上面的一排
}
// 下面处理其它的n-1个没有排序好的序列,送出最小的数字使其失去参选的资格
for (i = 0; i < n - 1; i++) {
a[i] = tree[0].data;//得到最小的数字
tree[tree[0].index].active = false;// 该节点失效,只需要标记最下面的节点是否是失效的就可以了
updateTree(tree, tree[0].index);
}
a[n - 1] = tree[0].data;
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
int b[] = { 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 };
tournamentSort(b, 16, 4);
for (int i = 0; i < 16; i++) {
cout << b[i] << " ";
}
cout << "\n";
//int b[] = { 4,3,2,5,6,6,7,8 };
//tournamentSort(b, 8, 3);
//for (int i = 0; i < 8; i++) {
// cout << b[i] << " ";
//}
//cout << "\n";
}
Heap sorting
-
锦标赛排序的坏处是使用了过多的辅助的空间,并且给较大的元素进行了多次的比较,于是有了堆排序。
-
堆的定义:n个元素的序列 a 0 , a 1 , . . . , a n − 1 a_0,a_1,...,a_{n-1} a0,a1,...,an−1当且仅当满足如下的关系的时候,称之为堆:
f ( x ) = { k i < k 2 i + 1 k i < k 2 i + 2 f(x)= \begin{cases} k_i<k_{2i+1}\\ k_i<k_{2i+2}\\ \end{cases} f(x)={ki<k2i+1ki<k2i+2
f ( x ) = { k i > k 2 i + 1 k i > k 2 i + 2 f(x)= \begin{cases} k_i>k_{2i+1}\\ k_i>k_{2i+2}\\ \end{cases} f(x)={ki>k2i+1ki>k2i+2
的时候,称为堆。
-
堆排序的实现:
-
如何由一个无序的序列建成一个堆,自底向上调整。
-
输出堆顶的元素之后,如何调整剩余的元素使其成为一个新的堆,自上向下调整。
输出堆顶的元素之后,堆顶用堆中最后一个元素替代,然后自顶向下的进行调整,使其满足堆的性质。首先堆顶的元素和左右的孩子进行比较,然后把比较小的孩子上升到堆顶部(假设是最小堆),假设该孩子是左孩子,那么左孩子的根发生了变化,需要重复上述的调整的过程(递归的过程),直到调整到根部的元素比两个孩子的元素都要小(相等)或者是到达叶子节点没有两个孩子为止。
-
-
堆排序是Unstable不稳定的排序算法,因为每一次把最后的一个元素提到了前面,后续调整堆的时候不知道会不会被调整到相等值数据的前面。
-
堆排序的比较次数为: 2 n ( ⌊ l o g 2 n ⌋ ) 2n(\lfloor log_2n\rfloor) 2n(⌊log2n⌋),最坏的情况下,时间复杂度也是 O ( n l o g n ) O(nlogn) O(nlogn),只需要一个记录大小的辅助空间。
-
具体的实现,假设待排序的数字的个数为 n n n,按照完全二叉树的形式组织这些数字编号为 0 − ( n − 1 ) 0-(n-1) 0−(n−1),编号最大的非终端的节点其实就是第 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n⌋个元素,也就是编号为 ⌊ n 2 ⌋ + 1 \lfloor \frac{n}{2} \rfloor+1 ⌊2n⌋+1的元素,因此筛选需要从这一个元素开始,一致筛选到第一个元素。
堆排序的具体的实现。
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN = 10005;
// 最大堆得到最小排列
void sift(vector<int>& pvector, int i, int n) {
int child, tmp;
tmp = pvector[i];
child = 2 * i + 1;// 得到左孩子节点
while (child < n) {
// 三个节点选择小的放到父亲节点上
// 如果右孩子小就选择右孩子
if ((child < n - 1) && (pvector[child] < pvector[child + 1])) {
child++;
}
// 父亲的值和孩子进行比较
if (tmp < pvector[child]) {
// swap(pvector[child],pvector[i]);
pvector[i] = pvector[child];
i = child;// 说明需要向孩子为根的子树上调整
child = 2 * i + 1;
}
else break;// 说明不需要向下进行调整
}
// 跳出的条件一个是没有孩子节点了,也就是到达了叶子节点
// 第二个可能的情况就是当前以i为根的子树不需要进行额外的调整了
pvector[i] = tmp;
}
// 数组的编号从1开始
void heapSort(vector<int>& pvector) {
int n = pvector.size();
// 下面首先建立堆,这里序号是从0开始的,所以减去n/2-1,i-1
for (int i = n / 2 - 1; i >= 0; i--) {
sift(pvector, i, n);//从i号节点一直向下维护好堆,当前还剩余n个长度大小的堆需要维护。
}
// 接下来不断的删除堆顶的元素,不断的取出元素
for (int i = n - 1; i > 0; i--) {
swap(pvector[0], pvector[i]);// 最后一个元素放到堆顶进行调整
sift(pvector, 0, i - 1);
}
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
int b[] = { 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 };
//tournamentSort(b, 16, 4);
heapSort(a);
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
//int b[] = { 4,3,2,5,6,6,7,8 };
//tournamentSort(b, 8, 3);
//for (int i = 0; i < 8; i++) {
// cout << b[i] << " ";
//}
//cout << "\n";
}
Merging based sorting
2-way merging based sorting
假设初始的序列的长度为n,看成是长度为n的子序列,每个子序列的长度为1,然后我们两两子序列进行归并,得到长度为 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil ⌈2n⌉个长度为2或者1的有序的子序列。如此重复直到得到程度为n的有序的序列为止。
- Stable的算法,但是很少适用于内部排序。
- 归并排序可以采用顺序存储结构和静态链表的存储结构。
归并排序的一个实现,采用顺序存储结构:
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度 O ( n ) O(n) O(n),这个 O ( n ) O(n) O(n)的空间主要用来辅助归并,栈的使用也需要使用额外的空间。
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN = 10005;
void mergeSort(vector<int>& v, int left, int right, vector<int>vt) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
mergeSort(v, left, mid, vt);
mergeSort(v, mid + 1, right,vt);
int pleft = left, pright = mid + 1;
int pmerge = left;
while (pleft <= mid && pright <= right) {
if (v[pleft] <= v[pright]) {
vt[pmerge++] = v[pleft++];
}
else{
vt[pmerge++] = v[pright++];
}
}
while (pleft <= mid) {
vt[pmerge++] = v[pleft++];
}
while (pright <= right) {
vt[pmerge++] = v[pright++];
}
for (int i = left; i <= right; i++) {
v[i] = vt[i];
}
}
void mergeBasedSort(vector<int>& v) {
int n = v.size();
int left = 0, right = n - 1;
vector<int>vt(v.size());
mergeSort(v, left, right,vt);
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
int b[] = { 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 };
mergeBasedSort(a);
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
}
采用静态链表的方式实现归并排序,实际上便省去了复制和移动元素的时间。
2021.3.16日需要去实现。
链表的归并排序和静态链表的归并排序大抵类似,这里先实现了一般单向链表的归并排序。
具体的思路是:
-
排序的思想和数组为存储结构的归并排序是一致的,每次将链表一拆为二,分别对左右两边进行排序。
-
链表排序和数组排序的不同有:
- 无法轻易的找到一段链表中间的节点,这个我们可以用快慢指针来实现,初始设置一个 s l o w slow slow和 f a s t fast fast的指针, s l o w = l e f t , f a s t = l e f t − > r i g h t ( 列 表 只 有 一 个 元 素 的 情 况 特 殊 处 理 ) slow=left,fast=left->right(列表只有一个元素的情况特殊处理) slow=left,fast=left−>right(列表只有一个元素的情况特殊处理),然后我们:
while (fast != right) { fast = fast->next; if (fast == right) { break; } slow = slow->next; fast = fast->next; }
这样的话我们就可以从 s l o w slow slow处去划分,将 [ l e f t , r i g h t ] [left,right] [left,right]划分为 [ l e f t , s l o w ] [left,slow] [left,slow]和 [ s l o w − > n e x t , f a s t ] [slow->next,fast] [slow−>next,fast]。
- 将 [ l e f t , r i g h t ] [left,right] [left,right]划分为 [ l e f t , s l o w ] [left,slow] [left,slow]和 [ s l o w − > n e x t , f a s t ] [slow->next,fast] [slow−>next,fast]之后对 [ l e f t , s l o w ] [left,slow] [left,slow]和 [ s l o w − > n e x t , f a s t ] [slow->next,fast] [slow−>next,fast]分别进行排序,排序过后 l e f t , s l o w , s l o w − > n e x t 和 f a s t left,slow,slow->next和fast left,slow,slow−>next和fast指向的值很可能发生变化,因此 m e r g e S o r t ( l e f t , r i g h t ) mergeSort(left,right) mergeSort(left,right)的两个参数改成引用的类型,便于修改,值得注意的是,引用类型不能传入 s l o w − > n e x t slow->next slow−>next这样的参数,否则会导致前面排好序的链表中的某个元素的 n e x t next next被神奇的修改掉。
-
下面给出详细的代码:
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<stack>
using namespace std;
const int MAXN = 10005;
struct Node {
int val;
Node* next;
Node(int _val = 0, Node* _next = NULL) {
val = _val;
next = _next;
}
};
struct Head {
Node* head;
int n;
Head(Node* _head = NULL, int _n = 0) {
head = _head;
n = _n;
}
};
void mergeSort(Node*& left, Node*& right) {
if (left == right) {
return;
}
// 至少两个元素
Node* slow = left, * fast = left->next;
while (fast != right) {
fast = fast->next;
if (fast == right) {
break;
}
slow = slow->next;
fast = fast->next;
}
Node* mid = slow; // 分成[left,mid]和[mid+1,right]
Node* mid_1 = slow->next;
// 排完序之后left和mid都可能会被改掉
mergeSort(left, mid);
// 排完序之后left和mid都可能会被改掉
mergeSort(mid_1, right);
// 修改mid->next=NULL
// 修改right->next=NULL
// 为了下面方便合并
mid->next = NULL;
right->next = NULL;
Node* head = NULL, * cur = NULL;
Node* p1 = left, * p2 = mid_1;
// 因为不知道p1的后面是什么元素,所以最后一个元素特殊处理
while (p1 != NULL && p2 != NULL) {
if (p1->val <= p2->val) {
if (head == NULL) {
cur = p1;
head = cur;
}
else {
cur->next = p1;
cur = cur->next;
}
p1 = p1->next;
}
else {
if (head == NULL) {
cur = p2;
head = cur;
}
else {
cur->next = p2;
cur = cur->next;
}
p2 = p2->next;
}
}
while (p1 != NULL) {
cur->next = p1;
cur = cur->next;
p1 = p1->next;
}
while (p2 != NULL) {
cur->next = p2;
cur = cur->next;
p2 = p2->next;
}
// 改掉left和right
left = head;
right = cur;
return;
}
void linkedListMergeSort(Head& head) {
if (head.head == NULL) {
return;
}
// 下面找到最右端的节点使right指向这个Node
Node* left = head.head, * right = head.head;
Node* lleft = NULL;// 最右端节点的前面的一个节点
while (right != NULL) {
lleft = right;
right = right->next;
}
right = lleft;
mergeSort(left, right);
head.head = left;
}
int main() {
//vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
vector<int> a({ 7,5,1,2,3,4 });
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
Head head;
// 建立链表
for (int i = 0; i < a.size(); i++) {
Node* node = new Node();
node->val = a[i];
node->next = head.head;
head.head = node;
}
linkedListMergeSort(head);
Node* node = head.head;
while (node) {
cout << node->val << " ";
node = node->next;
}
cout << "\n";
}
Radix sorting
基数排序和之前说到的排序的方法都不大一样,前面排序的方法的基本操作都是比较和移动记录,基数排序借助于多关键字排序思想对单逻辑关键字进行排序的方法,不需要进行关键字的比较。
多关键字排序:假设序列 ( R 1 , R 2 , . . . , R n ) (R_1,R_2,...,R_n) (R1,R2,...,Rn),每个 R i R_i Ri中含有 d d d个关键字 ( k i 0 , k i 1 , . . . , k i d − 1 ) (k_i^0,k_i^1,...,k_i^{d-1}) (ki0,ki1,...,kid−1),该序列对关键字有序是指,对任意的 R i , R j , 1 < = i < j < = n R_i,R_j,1<=i<j<=n Ri,Rj,1<=i<j<=n,都有:
( k i 0 , k i 1 , . . . , k i d − 1 ) < ( k j 0 , k j 1 , . . . , k j d − 1 ) (k_i^0,k_i^1,...,k_i^{d-1})<(k_j^0,k_j^1,...,k_j^{d-1}) (ki0,ki1,...,kid−1)<(kj0,kj1,...,kjd−1)
k 0 k^0 k0称为主关键字,其余的称为次关键字。
最高位优先法:先按照 k 0 k^0 k0排序,然后 k 1 k^1 k1,…,最后 k d − 1 k^{d-1} kd−1
最低位优先法:先按照 k d − 1 k^{d-1} kd−1排序,然后 k d − 2 k^{d-2} kd−2,…,最后 k 0 k^{0} k0
注意:从低位到高位必须使用稳定的排序方法,例如12和13排序,低位排完后,我们得到12在13的前面,这个时候我们需要排1这一位,这个时候如果采用不稳定的排序算法,13可能被排到12的前面,因此,从低位到高位排序必须使用稳定的排序算法。
从高位到地位排序则不必,因为10进制的数字,高位排完序之后,下面是按照每一个高位分为10个桶,在每一个桶里面重复刚才的排序的过程。
时间复杂度:假设有d个关键字,每一个关键字的取值范围为r,一趟分配的时间复杂度为 O ( n ) O(n) O(n),一趟收集的时间复杂度为 O ( r ) O(r) O(r),总的时间复杂度为** O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))**。
基数排序法某些情况下拥有更好的性能, 例如:有1,000,000个8位的整数要进行排序,使用快速排序法,至少需要nlog2n次比较,即2千万次,用基数排序法则只需要8*(1,000,000+10),约8百万次。
空间复杂度:增加了 n + 2 r , ( n 是 存 放 n 个 数 据 的 空 间 , 2 r 是 r 个 头 指 针 和 尾 指 针 ) n+2r,(n是存放n个数据的空间,2r是r个头指针和尾指针) n+2r,(n是存放n个数据的空间,2r是r个头指针和尾指针)的空间。
基本的过程:
-
分配
-
收集
首先以静态链表(cpp里面的vector<>也不错)存储n个待排列的记录,表头指针指向第一个记录,例如:
p→614→738→921→485→637→101→215→530→790→306
第一趟分配对最低数位关键字进行,改变记录指针值将链表中的记录分配至10个链队列中去,每个队列中的记录关键字的个位数相等;第一趟收集是改变所有非空队列的队尾的指针域,令其指向下一个非空队列的队头记录,重新将10个队列中的记录链成一个链表;第二趟分配和收集是针对十位数位进行的,过程和个位数位相同。其他位数一样。
实现的参考代码:
#pragma warning(disable:4996)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<stack>
using namespace std;
const int MAXN = 10005;
struct Node {
int val;
Node* next;
Node(int _val = 0, Node* _next = NULL) {
val = _val;
next = _next;
}
};
struct Head {
Node* head;
int n;
Head(Node* _head = NULL, int _n = 0) {
head = _head;
n = _n;
}
};
// 获取val右边第i位的数字
int getNum(int val, int i) {
int res = 0;
for (int j = 0; j <= i; j++) {
res = val % 10;
if (res == 0) {
return res;
}
val /= 10;
}
return res;
}
void radixSort(Head& head, int d) {
if (head.head == NULL) {
return;
}
Node* nodesFirst[10] = { NULL };// 头
Node* nodesCur[10] = { NULL }; // 当前最后的一个节点
Node* first = head.head;
Node* cur = first;
// 开始分配
for (int i = 0; i < d; i++) {
cur = first;
Node* curr = first;
while (cur != NULL) {
// 取出右边第i位
int val = cur->val;
int num = getNum(val, i);
if (nodesFirst[num] == NULL) {
nodesFirst[num] = cur;
nodesCur[num] = cur;
}
else {
nodesCur[num]->next = cur;
nodesCur[num] = nodesCur[num]->next;
}
cur = cur->next;
}
// 进行收集
int j = 0;
while (nodesFirst[j] == NULL) {
j++;
}
first = nodesFirst[j];
cur = nodesCur[j]; // 前面一个列别的最后的一个Node
j++;
for (; j < 10; j++) {
if (nodesFirst[j] != NULL) {
cur->next = nodesFirst[j]; // 两边连起来
cur = nodesCur[j];
}
}
cur->next = NULL;
curr = first;
while (curr != NULL) {
cout << curr->val << " ";
curr = curr->next;
}
cout << "\n";
memset(nodesFirst, 0, sizeof(nodesFirst));
memset(nodesCur, 0, sizeof(nodesCur));
}
head.head = first;
}
int main() {
vector<int> a({ 15,4,3,2,5,6,6,12,21,127,127,2,2,2,1,3 });
//vector<int> a({ 7,5,1,2,3,4 });
for (int i = 0; i < a.size(); i++) {
cout << a[i] << " ";
}
cout << "\n";
Head head;
// 建立链表
for (int i = 0; i < a.size(); i++) {
Node* node = new Node();
node->val = a[i];
node->next = head.head;
head.head = node;
}
radixSort(head, 3);
Node* node = head.head;
while (node) {
cout << node->val << " ";
node = node->next;
}
cout << "\n";
}
各种排序方法的总结
- 平均时间性能:以快速排序法最佳,但最坏情况下不如堆排序和归并排序;在n较大时,归并排序比堆排序快,但所需辅助空间最多。
- 简单排序以直接插入排序最简单,当下列中记录“基本有序” 或n值较小时,是最佳的排序方法。因此常和其他排序方法结合使用。