希尔排序
希尔排序(ShellSort) 的思路有点奇特, 或许是书上表述的不清或许是我理解歪了, 真正了解希尔排序是怎样一种排序方法花了了差不多一个小时. 现在理解透了, 觉得真是很妙.
按照所规定的步长, 覆盖全部元素, 跳跃着进行插入排序, 暂时忽略中间的元素. 这也许就是希尔排序的内涵.
大概分为两个过程
- 根据所给数据设定步长. (我用的是n不断除2向下取整的策略, 这个策略不是最优的)
- 对每个步长, 从开始到数据尾部进行跳跃式的插入排序. (每个元素都会参与)
演示
注意:
步长的最后一个一定是1, 也就是说, 最后进行的是一个完整的常规的插入排序, 但是因为前面所做的工作, 在这一步中, 所需的操作已经很少了. 这对理解希尔排序有一定帮助.
算法的复杂度在O(n^2)和O(nlog₂n)之间. 占用空间大概就是存储序列的空间+1.
C++代码
#include <iostream>
using namespace std;
const int maxn = 1005;
int n;
void ShellInsert(int *arr, int dk)
{
for (int i = dk + 1; i <= n; ++i) {
int j = i - dk;
arr[0] = arr[i];
for (; j > 0 && arr[0] < arr[j]; j -= dk) {
arr[j + dk] = arr[j];
}
arr[j + dk] = arr[0];
}
}
void ShellSort(int *arr, int *dlta, int size)
{
for (int i = 0; i < size; ++i) {
ShellInsert(arr, dlta[i]);
cout << "\n第 " << i + 1 << " 趟排序:\n";
for (int j = 1; j <= n; ++j) {
cout << arr[j] << ' ';
}
cout << endl;
}
}
int main()
{
int array[maxn];
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> array[i];
}
int tmp = n, k = 0, dlta[maxn];
while (tmp > 1) {
dlta[k++] = tmp / 2;
tmp /= 2;
}
ShellSort(array, dlta, k);
}
/*
8
49 39 65 97 76 13 27 69
7
7 1 5 4 3 6 2
10
49 38 65 97 76 13 27 49 55 04 // 5 3 1
*/
归并排序
这里介绍的归并排序是最基础的二路归并. 归并排序的思想很简单, 就是分治的思想.
分而治之, 把一个序列分成两段, 分别对这两个序列再排序. 不断地划分后, 最后的序列就是一个元素. 那么再依次往上合并.
使用递归非常方便实现. 也可以用非递归实现.
归并排序演示
以及
递归版本
有两个重要函数
- MergeSort()函数, 这个是归并排序的主体, 它解决的问题是下标从left到right的排序. 在程序中将会调用自身.
- Merge()函数, 这个函数被MergeSort()函数调用, 它解决的问题是将left和right的两个分别排好序的序列合并.
通过以上两个函数, 就可以实现归并排序了.
归并排序的时间复杂度最差是O(nlog₂n), 最优是O(n), 且是一种稳定排序
递归代码
#include <iostream>
using namespace std;
const int maxn = 1005;
void merge(int *A, int L1, int R1, int L2, int R2)
{
int tmp[maxn], index = 0;
int i = L1, j = L2;
while (i <= R1 && j <= R2) {
if (A[i] < A[j]) tmp[index++] = A[i++];
else tmp[index++] = A[j++];
}
while (i <= R1) tmp[index++] = A[i++];
while (j <= R2) tmp[index++] = A[j++];
for (int k = L1; k <= R2; ++k) {
A[k] = tmp[k - L1];
}
}
void mergeSort(int *A, int left, int right)
{
if (left < right) {
int mid = (left + right) / 2;
mergeSort(A, left, mid);
mergeSort(A, mid + 1, right);
merge(A, left, mid, mid + 1, right);
}
}
int main()
{
int A[maxn], n;
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> A[i];
}
mergeSort(A, 1, n);
cout << "排序结果:\n";
for (int i = 1; i <= n; ++i) {
cout << A[i] << ' ';
}
cout << endl;
}
/*
7
7 1 5 4 3 6 2
10
49 38 65 97 76 13 27 49 55 04
*/
非递归版本
思路是基本一样的.
非递归版本在选取的步长上做了些学问
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1005;
void Merge(int *A, int L1, int R1, int L2, int R2)
{
int i = L1, j = L2;
int index = 0, tmp[maxn];
while (i <= R1 && j <= R2) {
if (A[i] < A[j]) tmp[index++] = A[i++];
else tmp[index++] = A[j++];
}
while (i <= R1) tmp[index++] = A[i++];
while (j <= R2) tmp[index++] = A[j++];
for (int k = L1; k <= R2; ++k) {
A[k] = tmp[k - L1];
}
}
void MergeSort(int *A, int n)
{
for (int step = 2; step / 2 <= n; step *= 2) {
for (int i = 1; i <= n; i += step) {
int mid = i + step / 2 - 1;
if (mid + 1 <= n) // 右边要存在才行, 不要漏掉=
Merge(A, i, mid, mid + 1, min(n, i + step - 1)); // 不能大于n
}
}
}
int main()
{
int n, A[maxn];
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> A[i];
}
MergeSort(A, n);
cout << "排序结果:\n";
for (int i = 1; i <= n; ++i) {
cout << A[i] << ' ';
}
cout << endl;
}
/*
7
7 1 5 4 3 6 2
10
49 38 65 97 76 13 27 49 55 04
*/
堆排序
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
通常堆是通过一维数组来实现的。在数组起始位置为1的情形中:
父节点i的左子节点在位置为 2i
父节点i的右子节点在位置为 2i + 1
子节点i的父节点在位置 i / 2
利用这个性质可以很方便的进行父亲节点和子结点之间的跳转.
堆排序可以简化为两个过程
- 将一个序列堆化
- 输出堆顶元素, 然后交换堆顶元素和序列尾部元素, 总数-1, 再进行堆化.
可见, 随着不断输出堆顶元素,序列就已经排好序了.
第一个过程中,要进行n/2次向下调整,向下调整在代码中是用downAdjust函数实现.
第二个过程中,只要把交换上去的最后一个元素进行向下调整即可.
堆排序的时间复杂度是O(nlogn),空间复杂度是O(n)
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1005;
void downAdjust(int *A, int low, int high)
{
int i = low, j = 2 * low;
while (j <= high) {
if (j + 1 <= high && A[j] < A[j + 1]) {
j += 1;
}
if (A[i] < A[j]) {
swap(A[i], A[j]);
i = j;
j *= 2;
} else break;
}
}
void HeapSort(int *A, int n)
{
for (int i = n / 2; i >= 1; --i) {
downAdjust(A, i, n);
}
int m = n;
for (int i = 1; i <= m; ++i) {
cout << A[1] << ' ';
swap(A[1], A[n--]);
downAdjust(A, 1, n);
}
cout << endl;
}
int main()
{
int n, A[maxn];
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> A[i];
}
HeapSort(A, n);
}
/*
7
4 3 6 7 2 5 1
10
49 38 65 97 76 13 27 49 55 04
*/
如果需要插入新的元素的话, 可以使用upAdjust函数, 即向上调整. 时间复杂度为O(logn)
void upAdjust(int low, int high)
{
int i = high, j = i / 2;
while (j >= low) {
if (heap[j] < heap[i]) {
swap(heap[j], heap[i]);
i = j;
j = i / 2;
} else break;
}
}
快速排序
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。快速排序通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。
快速排序平均时间复杂度是O(nlogn), 最差是O(n²).
按照我的经历, 快速排序比较难写. 虽然代码简洁, 但是写出的代码非常非常容易出bug!里面有很多<, <= 等边界控制, 很容易出错的. 一般来说, 隔一段时间再写快排, 常常要参照以前的代码debug.
快排被研究得比较多, 有很多的优化, 感兴趣的可以做深入了解.
示例代码, 此代码是以中间数为基准点, 平均来说似乎比以区间第一个或最后一个为基准要快(根据做题的经验)
#include <iostream>
using namespace std;
const int maxn = 1005;
int n, A[maxn];
void QuickSort(int *A, int l, int r)
{
if (l >= r) return;
int mid = A[(l + r) >> 1];
int i = l, j = r;
while(i <= j) {
while (A[i] < mid) i++;
while (A[j] > mid) j--;
if (i <= j) swap(A[i++], A[j--]);
}
if (j > l) QuickSort(A, l, j);
if (i < r) QuickSort(A, i, r);
}
void show(int *A, int n)
{
for (int i = 0; i < n; ++i)
cout << A[i] << ' ';
cout << endl;
}
int main()
{
cin >> n;
for (int i = 0; i < n; ++i) {
cin >> A[i];
}
QuickSort(A, 0, n - 1);
show(A, n);
}
/*
10
85 55 82 57 68 92 99 98 66 56
4
43 101 96 -1
5
3 2 5 1 4
*/
以上就是四种基础的排序算法
希尔排序
归并排序
堆排序
快速排序
至于更基础的, 或者更复杂度, 都不在此博客讨论范围之内.