Table of Contents
简单排序
插入排序
- 序列分为已排序和未排序两部分。
- 取一个未排序元素x,将已排序的元素中比x大的元素后移,然后将 x 插入空位。
- 其外层循环遍历的是未排序部分,其内层循环遍历的是已排序部分。
- 数据越有序,效率越高。
void InsertionSort(int *seq, int N) {
for (int i = 1; i < N; i++) {
int v = seq[i];
int j = i-1;
//右移已排序序列寻找插入位置
while (seq[j] > v && j >= 0) {
seq[j+1] = seq[j];
j--;
}
seq[j+1] = v;
}
}
选择排序(不稳定)
- 序列分为已排序(且是最终的排序)和未排序两部分。
- 选择(导致其效率与数据无关)出未排序部分的最小值,与未排序部分的第一个元素交换(导致其是不稳定的,如5,6,5,2)。
- 其内层循环遍历的是未排序部分
- 元素交换次数少,一轮比较只需要换一次位置。这也是人工排序大多使用这种方法的原因。
void SelectionSort(int *seq, int n){//选择排序
for (int i = 0; i < n; i++) {
//寻找未排序部分的最小值
int minj = i;
for (int j = i + 1; j < n; j++) {
if (seq[j] < seq[minj]) {
minj = j;
}
}
//交换
swap(seq[i], seq[minj]);
}
}
冒泡排序
冒泡排序
- 序列分为已排序(且是最终排序)和未排序两部分。
- 每次从序列末尾开始挑更小的元素的一路冒到未排序部分的第一个元素
- 其内层循环遍历的是未排序部分。
- 这可以说是改进的选择排序,边比较边交换,而不是光看。到最后才进行一次破坏稳定性的远距离交换。
- 一个序列的逆序数是固定的,逆序数可以体现序列的错乱程度。每一次交换临近的元素,就消除该序列的一个逆序数对。
- 如果一路冒下去,一个交换的都没有,那么说明数组逆序数低到0,即已排序。此时可以中断。这种中断会带来很大的效率提升。
void BubbleSort(int *seq, int N) {
bool flag = false;//已排序标志
for (int i = 0; !flag; i++) {
flag = true;
for (int j = N - 1; j >= i + 1; j--) {
if (seq[j] < seq[j - 1]) {
swap(seq[j], seq[j - 1]);
flag = false;
}
}
}
}
复杂排序
希尔排序(不稳定)
- 就是按照不同步长g对元素进行插入排序。
- 最后g = 1,也就是简单插入排序,可以绝对保证序列顺序正确。
- g的生成规则是 g = 3*g + 1
- 当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,因为插入排序对于有序的序列效率很高,速度也很快。
- 其实不稳定的。因此,在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。
void InsertionSort(int *seq, int N, int G) {
for (int i = G; i < N; i++) { //i++保证了排序的完整性
int v = seq[i];
int j = i - G;
//右移已排序序列寻找插入位置
while (j >= 0 && seq[j] > v) {
seq[j + G] = seq[j];
j -= G;
}
seq[j + G] = v;
}
}
void ShellSort(int *seq, int N) {
//生成序列G
vector<int> G;
int g = 1;
while (g <= N) {
G.push_back(g);
g = 3 * g + 1;
}
//循环排序
for (int i = G.size() - 1; i >= 0; i--) {
InsertionSort(seq, N, G[i]);
}
}
归并排序
- 将大序列分割成两个小序列,分别对这两个小序列执行归并排序(递归)。然后merge两个小序列成一个已排序的大序列(合并)。
- 在合并两个序列时,只要保证前者优先于后者,那么相同元素的顺序就不会颠倒。使用归并排序是稳定的。
- merge的思想就是双队列循环取出更小的队首
- 排序是在递归的回溯阶段完成的
void Merge(int *seq, int start, int mid, int end, int *temp) {
int i = start;
int j = mid + 1;
int k = 0;
//双队列合并
while (i <= mid && j <= end) {
if (seq[i] <= seq[j]) { //体现了第一个子序列优先合并,保证了稳定性
temp[k++] = seq[i++];
} else {
temp[k++] = seq[j++];
}
}
while (i <= mid) {
temp[k++] = seq[i++];
}
while (j <= end) {
temp[k++] = seq[j++];
}
// 同步回原序列
for (i = 0; i < k; i++) {
seq[start + i] = temp[i];
}
}
void MergeSort(int *seq, int start, int end, int *temp) {
//二分递归
if (start < end) {
int mid = (start + end) / 2;
MergeSort(seq, start, mid, temp);
MergeSort(seq, mid + 1, end, temp);
Merge(seq, start, mid, end, temp);
}
}
void MergeSort(int *seq, int N) {
int *temp = new int[N]; //缓存副本
MergeSort(seq, 0, N - 1, temp);
delete[] temp;
}
快速排序(不稳定)
- 前一部分小于轴,后一部分大于轴。分割递归下去就排好了。
- Partition函数是核心,其选则一个数做为轴,最终使得轴左边的元素都小于轴,右边的元素都大于轴。
- Partiiton函数实现:选择最后一个元素作为轴,从头遍历序列,如果是大于轴的不用管,如果是小于等于轴的那么与大于轴的部分的第一个值做交换。最后,轴再和大于轴部分的第一个值做交换。
- 排序是在递归的回溯阶段完成的。
- 因为会盲目的交换非相邻元素,因此是不稳定的。
- 轴选择的不好的话,比如选中了最大值或最小值,那么一次分割只能分出1个,那么算法时间复杂度会上升到O(n^2),并且递归深度过大导致栈溢出 。但我们希望,一次分割尽可能能够一半一半分。
int Partition(int *seq, int start, int end) {//划分
int i = start - 1;
for (int j = start; j <= end - 1; j++) {
if (seq[j] <= seq[end]) {
i++;
swap(seq[j], seq[i]);
}
}
swap(seq[i + 1], seq[end]);
return i + 1;
}
void QuickSort(int *seq, int left, int right)
{
if (left < right) {
int p = Partition(seq, left, right);
QuickSort(seq, left, p - 1);
QuickSort(seq, p + 1, right);
}
}
void QuickSort(int *seq, int N) {
QuickSort(seq, 0, N - 1);
}