所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。
冒泡排序
冒泡排序(英语:Bubble Sort,台湾另外一种译名为:泡沫排序)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
最差时间复杂度 O(n^{2})
最优时间复杂度 O(n)
平均时间复杂度 O(n^{2})
最差空间复杂度 总共O(n),需要辅助空间O(1)
算法描述:
1.比较相邻的元素。如果第一个比第二个大,就交换它们两个;
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素3.应该会是最大的数;
4.针对所有的元素重复以上的步骤,除了最后一个;
5.重复步骤1~3,直到排序完成。
图片描述:
算法实现:
C语言:
void bubble_sort(int arr[], int len) {
int i, j, temp;
for (i = 0; i < len - 1; i++) //进行len-1次
for (j = 0; j < len - 1 - i; j++)//每进行一次排序,就会有一个数字在所在位置上,下一次就会少排一个数
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
C++:
#include <iostream>
#include <algorithm>
using namespace std;
template<typename T>//函数模板,保持一个好的通用性
void bubble_sort(T arr[], int len) {
int i, j;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1])
swap(arr[j], arr[j + 1]);
}
int main() {
int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
for (int i = 0; i < len; i++)
cout << arr[i] << ' ';
cout << endl;
float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 };
len = (int) sizeof(arrf) / sizeof(*arrf);
bubble_sort(arrf, len);
for (int i = 0; i < len; i++)
cout << arrf[i] << ' ';
return 0;
}
**适用场景**
冒泡排序思路简单,代码也简单,特别适合小数据的排序。但是,由于算法复杂度较高,在数据量大的时候不适合使用。
选择排序
工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
优点:
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其 最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
最差时间复杂度 О(n²)
最优时间复杂度 О(n²)
平均时间复杂度 О(n²)
算法描述:
1.在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2.从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3.重复第二步,直到所有元素均排序完毕。
算法实现:
C语言:
void SelectionSort(int* p, int len)
{
for (int i = 0; i < len - 1; ++i)
{
int m = 0; //选择第一个数假设他为最大
for (int j = 1; j < len - i; ++j) //从第二个数开始,每次进行排序下一次会少一个
{
if (p[j] > p[m])
{
m = j; //将比较后只比较打的数的下标
}
}
if (m != len - i - 1)
{
Swap(p + m, p + len - i - 1);
}
}
}
C++:
template<typename T>
void swap(T&x, T &y)
{
T temp=x;
x=y;
y=temp;
}
template<typename T>
void selection_sort(T arr[], int len) {
int i, j, min;
for (i = 0; i < len - 1; i++) {
min =i;
for (j = i + 1; j < len; j++)
if (arr[min] > arr[j])
min = j;
swap(arr[i], arr[min]);
}
}
适用场景
选择排序实现也比较简单,并且由于在各种情况下复杂度波动小,因此一般是优于冒泡排序的。在所有的完全交换排序中,选择排序也是比较不错的一种算法。但是,由于固有的O(n2)复杂度,选择排序在海量数据面前显得力不从心。因此,它适用于简单数据排序。
插入排序
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
最差时间复杂度 O(n^{2})
最优时间复杂度 O(n)
平均时间复杂度 O(n^{2})
最差空间复杂度 总共O(n) ,需要辅助空间O(1)
算法描述:
1.从第一个元素开始,该元素可以认为已经被排序
2.取出下一个元素,在已经排序的元素序列中从后向前扫描
3.如果该元素(已排序)大于新元素,将该元素移到下一位置
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5.将新元素插入到该位置后
6.重复步骤2~5
图形演示:
算法实现:
C语言:
void InsertSort(int* p, int len)
{
for (int j = 1; j < len; ++j)//第一个天然有序,从第二个开始插入
{
int t = p[j]; //将p【i】先赋值给他
int i = j - 1;//让指向j的前一个位置
while (i >= 0 && p[i] > t)
{
p[i + 1] = p[i]; //将元素逐个后移,以便找到插入位置时可立即插入
i--;
}
p[i + 1] = t;//被排序数放到正确的位置
}
}
C++:
template<typename T>
void insersort(T arr[], int len)
{
int i, j;
T temp;
for (i = 1; i < len; i++)
{
temp = arr[i];
for (j = i - 1; j >= 0 && arr[j] > temp; j--)
arr[j + 1] = arr[j];
arr[j + 1] = temp;
}
}
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
希尔排序
是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1.插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
2.但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
基本思想
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2=1(<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
图片:
![在这里插
算法实现:
static void InsertSortByShell(int* p, int len, int d)
{
for (int j = d; j < len; ++j)
{
int t = p[j];
int i = j - d;
while (i >= 0 && p[i] > t)
{
p[i + d] = p[i];
i-=d;
}
p[i + d] = t;
}
}
void ShellSort(int* p, int len)
{
for (int step = len / 2; step > 0; step /= 2)
{
InsertSortByShell(p, len, step);
}
}
归并排序
是创建在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
最差时间复杂度 O(nlog n)
最优时间复杂度 O(n)
平均时间复杂度 O(nlog n)
最差空间复杂度 O(n)
归并操作的过程如下:
1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2.设定两个指针,最初位置分别为两个已经排序序列的起始位置
3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4.重复步骤3直到某一指针到达序列尾
5。将另一序列剩下的所有元素直接复制到合并序列尾
图片:
代码实现:
static void Merge(int*p, int first, int mid, int last)
{
int* pbuf = (int*)malloc((last - first + 1) * sizeof(int));//开辟新数空间
int i, j, k;
i = first;//第一个数组下标
j = mid + 1;//第二个数组下标
k = 0;//新数组下标
if (pbuf != NULL)
{
while (i <= mid && j <= last)
{
if (p[i] < p[j])
{
pbuf[k++] = p[i++];
}
else
{
pbuf[k++] = p[j++];
}
}
while (i <= mid) //循环结束,还有剩余元素没有进行放置
pbuf[k++] = p[i++];
while(j<=last)
pbuf[k++] = p[j++];
for (i = first,k=0; i <= last;)
{
p[i++] = pbuf[k++];//将新数组元素放入p数组
}
free(pbuf);
}
}
static void MSort(int* p, int first, int last)
{//递归操作
if (first < last)
{
int mid = (last + first)/2;
MSort(p, first, mid);
MSort(p, mid + 1, last);
Merge(p, first, mid, last);//归并
}
}
void MergeSort(int* p, int len)
{
MSort(p, 0,len-1);
}
快速排序
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
最差时间复杂度 O(n^2)
最优时间复杂度 O(n log n)
平均时间复杂度 O(n log n)
步骤为:
1.从数列中挑出一个元素,称为 “基准”(pivot),
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法实现:
void QSort(int*p, int first, int last)
{
if (first >= last)//首元素下标大于尾元素下标表示结束
return;
int t = p[first];
int i = first;
int j = last;
while(i < j)
{
while (i<j && p[j] > t)
j--;
if(i < j)
p[i++] = p[j];//j为第一个小于t元素下标,放在i的位置
while (i<j && p[i] <= t)
i++; //从前往后找第一个大于t的元素下标
if(i<j)
p[j--] = p[i];i为第一个大于t元素下标,放在j的位置
}
p[i] = t; //当i==j时,停放在i或j的位置
//递归
QSort(p, first, i - 1);
QSort(p, j + 1, last);
}
void QuickSort(int* p, int len)
{
QSort(p, 0, len - 1);
}
动图演示:
快速排序并不是稳定的。这是因为我们无法保证相等的数据按顺序被扫描到和按顺序存放。
适用场景
快速排序在大多数情况下都是适用的,尤其在数据量大的时候性能优越性更加明显。但是在必要的时候,需要考虑下优化以提高其在最坏情况下的性能。
它的最坏情况是很恐怖的,需要
堆排序
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
最差时间复杂度 O(n logn)
最优时间复杂度 O(n logn)
平均时间复杂度 O(n log n)
通常堆是通过一维数组来实现的。在起始数组为 0 的情形中:
父节点i的左子节点在位置 (2i+1);
父节点i的右子节点在位置 (2i+2);
子节点i的父节点在位置 floor((i-1)/2);
在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:
最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build_Max_Heap):将堆所有数据重新排序
堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
算法实现:
static void AdjustHeap(int* p1, int len, int root)
{
int i = root * 2;// 为根的左子节点
int t = p1[root];//p1[root]根节点存在于t中
while (i <= len)
{
if (i+1 <= len && p1[i] < p1[i + 1])
{
i += 1; //左右进行比较,选择较大值的下标
}
if (p1[i] > t)
{
p1[i / 2] = p1[i];//将i指向的值给i的父亲节点
i *= 2;
}
else
{
break;
}
}
p1[i / 2] = t;
}
static void CreateHeap(int* p1, int len)
{
for (int i = len / 2; i > 0; --i) //建立堆
{
AdjustHeap(p1, len, i);
}
}
void HeapSort(int* p, int len)
{
CreateHeap(p - 1, len);
for (int i = 0; i < len - 1; ++i)
{
Swap(p, p + len - 1 - i);
AdjustHeap(p - 1, len - 1 - i, 1);
}
}