欢迎访问我的博客首页。
常用排序算法有插入排序、选择排序、交换排序三类。
插入排序、交换排序(冒泡排序)都是稳定的,且有最好和最坏时间;选择排序不稳定且只有最坏时间。它们的衍生算法:希尔排序、堆排序、快速排序都不稳定。
排序的操作有比较、移动、交换,这三个操作的耗时是递增的。。
插入排序在元素基本有序时最优。因为选择排序没有最优时间;冒泡排序需要进行完整的一轮两两比较操作,同时有交换操作。插入排序只需要部分比较操作,然后进行部分移动操作,最后进行一次交换操作。
快速排序使序列分段有序,减少移动和交换次数,所以快。
快速排序在元素基本有序时最差。元素基本有序时,每次选取的基准元素接近最小值或最大值,快速排序退化成冒泡排序。
2. O(nlogn) 的排序算法
堆排序不是稳定排序,但性能稳定且只需 O(1) 的额外空间。每轮排序能确定根结点位置。
快速排序不是稳定排序且性能不稳定,需要 O(nlogn) 的额外空间。每轮排序能确定基准元素位置。
归并排序是稳定排序且性能稳定,但需要 O(n) 的额外空间。每轮排序不能确定元素位置。
1. 插入排序
插入排序包括直接插入排序和希尔排序。直接插入排序是稳定的,代码如下:
template<typename T>
void insertSort(T* a, size_t n) {
for (int i = 1; i < n; i++) {
if (a[i] < a[i-1]) {
T temp = a[i];
int j;
// a[i]前面的元素后移1位。
for (j = i - 1; j >=0 && a[j] > temp; j--)
a[j + 1] = a[j];
a[j + 1] = temp;
}
}
}
2. 选择排序
选择排序包括直接选择排序和堆排序。两者是不稳定排序,但性能稳定:直接选择排序性能总是最差,堆排序总是 n l o g 2 n nlog_2n nlog2n。
2.1 直接选择排序
// a[i]被交换引起不稳定。
template<typename T>
void selectSort(T* a, size_t n) {
for (int i = 0; i < n - 1; i++) {
int min = i;
// a[i]前有序。从a[i]后面选择最小的元素放在a[i]。
for (int j = i + 1; j < n; j++)
if (a[j] < a[min])
min = j;
if (min != i)
swap(a[i], a[min]);
}
}
2.2 堆排序
堆排序使用的堆是完全二叉树,所以可以用数组存储。
从上图中的 (a) 可以看出,假如从 0 开始编号:结点 x 的左右子结点的编号分别是 2x+1、2x+2;假如结点总数是 n,最后一个非叶子的编号是 n 2 − 1 \frac{n}{2}-1 2n−1。
堆排序。
void HeapAdjust(int* arr, int parent, int n) {
int temp = arr[parent], child = 2 * parent + 1;
while (child < n) {
if (child + 1 < n && arr[child] < arr[child + 1])
child++;
if (temp > arr[child])
break;
arr[parent] = arr[child];
parent = child;
child = 2 * child + 1;
}
arr[parent] = temp;
}
void HeapSort(int* arr, int n) {
for (int i = n / 2 - 1; i >= 0; i--)
HeapAdjust(arr, i, n);
for (int i = n - 1; i > 0; i--) {
swap(arr[i], arr[0]);
HeapAdjust(arr, 0, i);
}
}
上面实现的是大顶堆,用于从小到大排序。如果要实现小顶堆,把第 4 行的第 2 个小于号改为大于号,且把第 6 行的大于号改为小于号
3. 交换排序
交换排序包括冒泡排序和快速排序。冒泡排序是稳定的,快速排序不稳定。
3.1 冒泡排序
template<typename T>
void bubbleSort(T* a, size_t n) {
for (int i = n - 1; i > 0; i--)
// a[i]后有序。
for (int j = 0; j < i; j++)
if (a[j] > a[j + 1])
swap(a[j], a[j + 1]);
}
冒泡排序每轮排好一个元素放在序列尾部,所以第一层循环是倒序的,第二层循环只排前部无序的部分。
3.2 快速排序
快速排序的主要部分是 partition 函数。partition 函数的作用是确定基准元素的位置,且使基准元素前面的元素都比它小,后面的元素都比它大(从小到大排序)。
一般以待排序列第一个元素作为基准元素。比如要对 0 到 9 这 10 个元素从小到大排序且第一个元素是 5。partition 执行一次后元素 5 就被放在第 6 个位置,且它前面的 5 个数都比它小,它后面的 4 个数都比它大。可以使用任意算法让基准元素前面的元素都比它小,后面的元素都比它大,比如:
- 从两个方向:把基准元素后方比它小的元素移到它前方,同时把基准元素前方比它大的元素移到它后方。
- 从一个方向:从基准元素的一端选择比它大或者小的元素放在它另一端。
下面把这两种算法都实现一下。我们先以第一个元素为基准元素实现两种算法,再以最后一个元素为基准元素实现两种算法。
1. 以第一个元素为基准元素
int partition_11(int* arr, int start, int end) {
int pivotkey = arr[start];
while (start < end) {
while (start < end && arr[end] >= pivotkey)
end--;
arr[start] = arr[end];
while (start < end && arr[start] <= pivotkey)
start++;
arr[end] = arr[start];
}
arr[start] = pivotkey;
return start;
}
int partition_21(int* arr, int start, int end) {
int pivotidx = end + 1;
for (int i = end; i > start; i--) {
if (arr[i] > arr[start]) {
pivotidx--;
if (pivotidx != i)
swap(arr[pivotidx], arr[i]);
}
}
pivotidx--;
swap(arr[pivotidx], arr[start]);
return pivotidx;
}
2. 以最后一个元素为基准元素
int partition_12(int* arr, int start, int end) {
int pivotkey = arr[end];
while (start < end) {
while (start < end && arr[start] <= pivotkey)
start++;
arr[end] = arr[start];
while (start < end && arr[end] >= pivotkey)
end--;
arr[start] = arr[end];
}
arr[start] = pivotkey;
return start;
}
int partition_22(int* arr, int start, int end) {
int pivotidx = start - 1;
for (int i = start; i < end; i++) {
if (arr[i] < arr[end]) {
pivotidx++;
if (pivotidx != i)
swap(arr[pivotidx], arr[i]);
}
}
pivotidx++;
swap(arr[pivotidx], arr[end]);
return pivotidx;
}
注意:partition 函数中 arr[start]、arr[end] 和 pivotkey 比较时至少有一个要带等号,而不能都用大于或小于。否则当 arr[start] 等于 are[end] 时三者相等,partition 函数的第一个 while 循环会进入死循环。
3. 调用 partition 的递归算法与非递归算法
void QuickSort(int* arr, int start, int end) {
if (start >= end)
return;
int pivotloc = partition(arr, start, end);
QuickSort(arr, start, pivotloc - 1);
QuickSort(arr, pivotloc + 1, end);
}
void QuickSort(int* arr, int start, int end) {
stack<int> st;
st.push(start);
st.push(end);
while (st.empty() != true) {
int end = st.top();
st.pop();
int start = st.top();
st.pop();
int pivotloc = partition(arr, start, end);
if (pivotloc - 1 > start) {
st.push(start);
st.push(pivotloc - 1);
}
if (pivotloc + 1 < end) {
st.push(pivotloc + 1);
st.push(end);
}
}
}
4. 归并排序
下面是使用归并排序从小到大排序的示意图。
图
3
归并排序
图 \ 3 \quad 归并排序
图 3归并排序
图 3 是归并排序示意图。归并排序是一个先拆分后合并的过程。拆分的过程对数组划分,合并的过程把两个无序数组合并成一个有序数组。下面先给出递归形式的归并排序,再给出非递归形式的归并排序。
void Merge(int* arr, int* temp, int start, int middle, int end) {
int p_left = start, p_right = middle + 1, p_temp = 0;
while (p_left <= middle && p_right <= end) {
if (arr[p_left] <= arr[p_right])
temp[p_temp++] = arr[p_left++];
else
temp[p_temp++] = arr[p_right++];
}
while (p_left <= middle)
temp[p_temp++] = arr[p_left++];
while (p_right <= end)
temp[p_temp++] = arr[p_right++];
memcpy(arr + start, temp, sizeof(int) * (end - start + 1));
}
void MSort(int* arr, int* temp, int start, int end) {
if (start >= end)
return;
int middle = (start + end) / 2;
MSort(arr, temp, start, middle);
MSort(arr, temp, middle + 1, end);
Merge(arr, temp, start, middle, end);
}
void MergeSort(int* arr, int n) {
int* temp = new int[n];
MSort(arr, temp, 0, n - 1);
delete[] temp;
}
代码:主要部分是 Merge 函数,该函数的作用是把两个有序数组 arr[start : middle] 和 arr[middle+1 : end] 合并成有序数组 temp[0 : end-start+1],再把 temp[0 : end-start+1] 拷贝到 arr[start : end]。所以归并排序需要一个和待排序列同样大小的额外空间。下面是非递归实现:
void MergeSort(int* arr, int n) {
int* temp = new int[n];
for (int seg = 1; seg < n; seg *= 2)
for (int start = 0; start < n; start += seg * 2) {
start = min(start, n - 1);
int middle = min(start + seg - 1, n - 1), end = min(start + seg * 2 - 1, n - 1);
Merge(arr, temp, start, middle, end);
}
delete[] temp;
}
非递归算法依然调用 Merge 函数。在 Merge 函数的第 2 行,middle 被划分到左侧,所以非递归算法的第 6 行为了保存一致,也都把 middle 划分到左侧。
5. 临时
- 选择排序:每轮确定一个前部元素。比较次数 = n-1 + n-2 + … + 1。最小交换次数 = 0,最大交换次数 = n-1。
因为比较次数固定,所以时间恒为 O(n^2)。因为交换,所以不稳定。 - 插入排序:每轮确定一个前部元素。最小比较次数 = n-1,最大比较次数 = 1 + 2 + … + n-1。最小移动次数 = 0,最大移动次数 = 1 + 2 + … + n-1。
时间不恒定。因为移动,所以稳定。 - 冒泡排序:每轮确定一个后部元素。最小比较次数 = n-1,最大比较次数 = n-1 + n-2 + … + 1。最小交换次数 = 0,最大交换次数 = n-1 + n-2 + … + 1。
时间不恒定。因为是相邻交换,所以稳定。 - 快速排序:每轮确定一个中轴元素。最小比较次数 = n-1,最大比较次数 = n-1 + 2((n-1)/2) + 4