最近对内部排序进行复习,深入掌握各种排序算法的思想、排序过程和特征,对一些常用排序算法的关键代码进行比较,最终为了在工作中看到特定序列有选择最优排序算法的能力。
一、直接插入排序
这种排序的思想是,在待排序的数组中,设置一段排序区间让他始终保持有序并不断增大,直到覆盖整个数组,就完成了对数组的排序。
这就是最简单的直接插入排序,它的时间复杂度 是 O(N)~O(N^2)
当待排序集合本身接近有序时,它的时间复杂度最小,
当待排序集合本身时逆序集合时,它的时间复杂度最大
具体代码如下:
void DirectInsertionSort(int *arr, int sz)
{
assert(arr);
if (sz <= 1)
{
return;
}
for (int i = 0; i < sz - 1; i++)
{
if (arr[i + 1] < arr[i])
{
//end之前的元素都是有序的,tmp表示这个集合接下来的一个元素
int tmp = arr[i + 1];
int end = i;
//将tmp放到排序数组中合适的位置
while (end >= 0 && arr[i] > tmp)
{
arr[end + 1] = arr[end];
end--;
}
arr[end + 1] = tmp;
}
}
}
二、希尔排序
简单的说,希尔排序还是插入排序的一种
直接插入算法 比较适合用于接近顺序的数组,不适合于接近完全逆序的数组,
而希尔排序的过程,则是让数组变得越来越有序化,直到完全有序的过程
具体步骤如下:
1、指定一个间隔,gap将数组划分成若干组: (0, gap, gap+gap, …) (1, gap+1, gap+gap+1, …)
分别对每一组的最后一个元素进行单趟插入
2、不断减小gap,重复上述的步骤
3、直到gap为1时,相当于一次普通,但比较高效的的插入排序
根据这样的算法,可以得出结论:
当数组是完全逆序的情况下,希尔排序的优化程度最高
当数组是完全有序的情况下,希尔排序没有优化
时间复杂度为O(N) ~ O(N^2) 但实际上,几乎不会慢到O(N^2)的情况
具体代码如下:
void ShellSort(int *arr, int sz)
{
assert(arr);
if (sz <= 1)
{
return;
}
int gap = sz;
while (gap > 1)
{
gap = gap / 3 + 1;
//将数组分割成若干部分,并分别对每部分进行一趟排序
for (int i = 0; i < sz - gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0 && tmp < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
arr[end + gap] = tmp;
}
}
}
三、冒泡排序
冒泡排序,每一趟都遍历一次待排序的数组,当遇到当前元素比下一个元素大的情况,则交换这两个元素
所以,其效率比直接选择排序略高
经过优化后,最快能达到 O(N)
所以其时间复杂度为 O(N) ~ O(logN)
具体代码如下:
void BubbleSort_1(int *arr, int sz)
{
assert(arr);
if (sz <= 1)
{
return;
}
for (int i = 0; i < sz - 1; ++i)
{
bool flag = true;
for (int j = 0; j < sz - i - 1; ++j)
{
if (arr[j] > arr[j + 1])
{
std::swap(arr[j], arr[j + 1]);
flag = false;
}
}
if (flag == true)
{
return;
}
}
}
四、快速排序
快速排序的思想,举个例子说明,待排序的数组如下:
2 5 1 6 4 7 3
进行快速排序,每趟排序之后的数组分别为:
[ 2 1 ] 3 [ 6 4 7 5 ]
1 2 3 [4] 5 [7 6]
1 2 3 4 5 6 7
其中这些红色的元素,左边都是比他小的数,右边都是比它大的数。
以上就是快速排序的思想:
每次从数组中提取一个元素出来,放在合适的位置,满足该位置左边的数都小于它本身,而该位置右边的数都大于它本身。
以上是单趟排序,经过单趟排序,一个元素已经放在了合适的位置,并且以该元素的值为分界,有2个区间,再对以上两个区间进行如上的单趟排序,当两个区间都没有元素的时候,整个数组就已经有序了。
具体代码如下:
//返回中间值的索引
int GetMidIndex(int *arr, int left, int right)
{
int mid = left + (right - left) / 2; //2
if (arr[left] < arr[mid])
{
if (arr[mid] < arr[right])
{
return mid;
}
else if (arr[left] < arr[right])
{
return right;
}
else
{
return left;
}
}
else //arr[left] > arr[mid]
{
if(arr[mid] > arr[right])
{
return mid;
}
else if (arr[left] > arr[right])
{
return right;
}
else
{
return left;
}
}
}
//单趟排序
int _PortSort(int *arr, int left, int right)
{
assert(arr);
//优化
int mid = GetMidIndex(arr, left, right);
std::swap(arr[mid], arr[right]);
int key = arr[right];
int begin = left;
int end = right - 1;
while (begin < end)
{
//begin找比key大的为止
while (begin < end && arr[begin] < key)
{
++begin;
}
//end找比key小的为止
while (begin < end && arr[end] >= key)
{
--end;
}
//找到了
if (begin < end)
{
std::swap(arr[begin], arr[end]);
}
}
if (arr[begin] > arr[right])
{
std::swap(arr[begin], arr[right]);
return begin;
}
else //这种情况下,说明本趟排序没有找到合适的位置
{
return right;
}
}
void _QuickSort(int *arr, int left, int right)
{
if (left >= right)
{
return;
}
int div = _PortSort(arr, left, right); //单趟排序,得到中间位置
_QuickSort(arr, left, div - 1);
_QuickSort(arr, div + 1, right);
}
void QuickSort(int *arr, int sz)
{
assert(arr);
if (sz <= 1)
{
return;
}
_QuickSort(arr, 0, sz - 1);
}
快速排序的优化主要为以下两方面:
1、“三次取中”
2、为了节约压栈开销,当递归若干次,区间已经足够小时,对小区间的处理不要继续递归调用了,而是用其他排序(如插入排序)
快速排序的时间复杂度是:O(logN) ,最慢有可能达到O(N^2),不过采用“三次取中”优化的快速排序,是可以避免这种最坏情况的
五、选择排序
选择排序是很好理解的一种排序算法,但也是最慢的一种排序算法。
它的实现步骤是:
选最大的元素放到队尾,再在剩下的元素中选最大的放之前队尾的前一个位置
所以它的时间复杂度 始终是 O(N^2)
具体代码如下:
void SelectionSort_Normal(int *arr, int sz)
{
assert(arr);
if (sz <= 1)
{
return;
}
while (sz > 0)
{
int MaxIndex = 0;
for (int i = 0; i < sz; i++)
{
if (arr[i] > arr[MaxIndex])
{
MaxIndex = i;
}
}
if (MaxIndex != (sz - 1))
{
std::swap(arr[sz - 1], arr[MaxIndex]);
}
sz--;
}
}
实现速度较慢,做一个优化每次顺便选出最小的元素放到当前的第一个位置,提高一倍的速度,从时间复杂度的角度来看,依然是 O(N^2)
具体代码如下:
/*
选择排序——直接选择排序2(每次选最大的数放到队尾,选最小的数放队首)
时间复杂都为 O(N^2 / 2) (效率翻倍)
*/
void SelectionSort_Improve(int *arr, int sz)
{
assert(arr);
if (sz <= 1)
{
return;
}
int front = 0;
int end = sz - 1;
while (front < end)
{
int MaxIndex = front;
int MinIndex = front;
for (int i = front; i <= end; ++i)
{
if (arr[i] > arr[MaxIndex])
{
MaxIndex = i;
}
if (arr[i] < arr[MinIndex])
{
MinIndex = i;
}
}
int MaxVal = arr[MaxIndex];
int MinVal = arr[MinIndex];
if (MaxIndex != end)
{
std::swap(arr[MaxIndex], arr[end]);
}
//修正MinIndex
if (MinIndex == end)
{
MinIndex = MaxIndex;
}
if (MinIndex != front)
{
int tmp = arr[front];
arr[front] = MinVal;
arr[MinIndex] = tmp;
}
++front;
--end;
}
}
六、堆排序
堆排序也是选择排序,上述的选择排序每次都得在剩下的元素中选择最大的元素并放在最后
而堆排序则借助堆,利用堆的特性,在选择最大元素这一步大大提高了效率
堆排序是一种很高效的算法,堆是一种数据结构,以二叉树的形式表现出来
堆又分为大堆、小堆,其根节点分别是所存放元素中最大、最小的
二叉树可以用指针的形式表示,也可以用数组下标的形式表示,这里采用下标的形式表示
排序的思路为:
(这里采用大堆的形式)
1、将所有元素放入堆中
2、这时堆顶元素就是最大的,将它与堆中最后一个元素交换,此时数组中最后一个元素就是所有元素中最大的那一个,接下来对堆顶的元素执行一次向下调整,将其放在合适的位置
(以上是一趟排序)
3、不断重复第2步,直到所有元素有序 ,
每次选出当前堆中最大的元素放在最后面,因此最终得到的是升序的数组。
堆排序效率很高,并且效率几乎不受数组本身的影响,是一种比较高效的排序算法。
其时间复杂度为 O(log2(N))
具体代码如下:
//向下调整
void AdjustDown(int *arr, int sz, int parent)
{
int child = parent * 2 + 1;
while (child < sz)
{
if (child + 1 < sz && arr[child + 1] > arr[child])
{
++child;
}
if (arr[child] > arr[parent])
{
std::swap(arr[child], arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int *arr, int sz)
{
assert(arr);
if (sz <= 1)
{
return;
}
//建堆
for (int i = (sz - 2) / 2; i >= 0; --i)
{
AdjustDown(arr, sz, i);
}
int end = sz - 1;
while (end > 0)
{
std::swap(arr[0], arr[end]);
AdjustDown(arr, end, 0);
--end;
}
}
七、归并排序
归并排序是用分治思想,分治模式在每一层递归上有三个步骤:
- 分解(Divide):将n个元素分成个含n/2个元素的子序列。
- 解决(Conquer):用合并排序法对两个子序列递归的排序。
- 合并(Combine):合并两个已排序的子序列已得到排序结果。
具体代码如下:
void Merge(int *a, int *tmp, int begin1, int end1, int begin2, int end2)
{
int index = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index] = a[begin1];
++begin1;
}
else
{
tmp[index] = a[begin2];
++begin2;
}
++index;
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
}
void _MergeSort(int *a, int *tmp, int left, int right)
{
if (left >= right)
{
return;
}
int mid = left + (right - left) / 2;
_MergeSort(a, tmp, left, mid);
_MergeSort(a, tmp, mid + 1, right);
Merge(a, tmp, left, mid, mid + 1, right);
memcpy(a + left, tmp + left, (right - left + 1) * sizeof(int));
}
void MergeSort(int *a, size_t n)
{
int *tmp = new int[n];
_MergeSort(a, tmp, 0, n - 1);
delete[] tmp;
}
八、基数排序
基数排序非常特别,不是基于比较进行排序,而是采用多关键字排序,也就是对关键字的各位的大小进行排序,分为最高位优先(MSD)和最低位优先(LSD)。
具体代码如下:
#define MAX 20
//#define SHOWPASS
#define BASE 10
void print(int *a, int n) {
int i;
for (i = 0; i < n; i++) {
printf("%d\t", a[i]);
}
}
void radixsort(int *a, int n) {
int i, b[MAX], m = a[0], exp = 1;
for (i = 1; i < n; i++) {
if (a[i] > m) {
m = a[i];
}
}
while (m / exp > 0) {
int bucket[BASE] = { 0 };
for (i = 0; i < n; i++) {
bucket[(a[i] / exp) % BASE]++;
}
for (i = 1; i < BASE; i++) {
bucket[i] += bucket[i - 1];
}
for (i = n - 1; i >= 0; i--) {
b[--bucket[(a[i] / exp) % BASE]] = a[i];
}
for (i = 0; i < n; i++) {
a[i] = b[i];
}
exp *= BASE;
#ifdef SHOWPASS
printf("\nPASS : ");
print(a, n);
#endif
}
}
int main() {
int arr[MAX];
int i, n;
printf("Enter total elements (n <= %d) : ", MAX);
scanf("%d", &n);
n = n < MAX ? n : MAX;
printf("Enter %d Elements : ", n);
for (i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
printf("\nARRAY : ");
print(&arr[0], n);
radixsort(&arr[0], n);
printf("\nSORTED : ");
print(&arr[0], n);
printf("\n");
return 0;
}
总结
类别 | 排序方法 | 平均情况 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
插入排序 | 插入排序 | 0(NA2) | 〇(N) | 0(NA2) | 0⑴ | 稳定 |
- | Shell排序 | 0(NA1.3) | 〇(N) | 0(NA2) | 0⑴ | 不稳定 |
选择排序 | 选择排序 | 0(NA2) | 0(NA2) | 0(NA2) | 0⑴ | 不稳定 |
- | 堆排序 | 0(N*lgN) | 0(N*lgN) | 0(N*lgN) | 0⑴ | 不稳定 |
交换排序 | 冒泡排序 | 0(NA2) | 〇(N) | 0(NA2) | 0⑴ | 稳定 |
- | 快速排序 | 0(N*lgN) | 0(N*lgN) | 0(NA2) | O(lgN) | 不稳定 |
归并排序 | 归并排序 | 0(N*lgN) | 0(N*lgN) | 0(N*lgN) | 〇(N) | 稳定 |