排序算法
1.快速排序
原理
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
① 从数列中挑出一个元素,称为 “基准”(pivot),
② 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
③ 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
将其中一个值作为比较值,将数组中小于该值的元素放在左边,大于该值的元素放在右边,从头开始对每个元素进行这个操作,遍历完成排序完成。返回值为当前比较值元素的下标。
效率分析
平均时间复杂度:O(NlogN)
最佳时间复杂度:O(NlogN)
最差时间复杂度:O(N^2)
空间复杂度:根据实现方式的不同而不同
时间复杂度分析>>
实现
int hoare(T a[],int l,int h)
{
int i,j;
T x;
i = l;
j= h;
x.key = a[i].key;
do
{
//例如升序排列 当左边不满足小于x时,被放置到右边上次遍历到的元素,因为右边上次遍历到的元素不符合大于x而被跳出,这样左边元素覆盖过来就符合条件,并在当前方向继续遍历
while((i<j)&&(a[j].key>=x.key))
{
j--;
}
if(i<j) //则由while循环的条件可知a[j].key<x.key)
{
a[i].key = a[j].key;
i++;
}
while(i<j&&(a[i].key<= x.key))
{
i++;
}
if(i<j)
{
a[j].key = a[i].key;
j--;
}
}while(i<j);
a[i].key = x.key;
return i;
}
递归调用
//递归调用排序算法
template<class T>
void quicksort2(T a[],int l,int h)
{
int i ;
if(l<h)
{
i = hoare(a,l,h);
quicksort2(a,l,i-1);
quicksort2(a,i+1,h);
}
}
2.冒泡排序
原理
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
通过两层循环控制:
第一个循环(外循环),负责把需要冒泡的那个数字排除在外;
第二个循环(内循环),负责两两比较交换。。
性能分析
平均时间复杂度:O(N^2)
最佳时间复杂度:O(N)
最差时间复杂度:O(N^2)
空间复杂度:O(1)
排序方式:In-place
稳定性:稳定
解析说明:
冒泡排序涉及相邻两两数据的比较,故需要嵌套两层 for 循环来控制;
外层循环 n 次,内层最多时循环 n – 1次、最少循环 0 次,平均循环(n-1)/2;
所以循环体内总的比较交换次数为:n*(n-1) / 2 = (n^2-n)/2 ;
按照计算时间复杂度的规则,去掉常数、去掉最高项系数,其复杂度为O(N^2) ;
最优的空间复杂度为开始元素已排序,则空间复杂度为 0;
最差的空间复杂度为开始元素为逆排序,则空间复杂度为 O(N);
平均的空间复杂度为O(1) .
注:
n:数据规模
k:”桶”的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
实现
流程:设数组长度为N。
1.比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换。
2.这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1个位置。
3.N=N-1,如果N不为0就重复前面二步,否则排序完成。
template <class T>
void bubblesort(int a[], int n)
{
int i, j;
for (i = 0; i < n; i++)
for (j = 1; j < n - i; j++)
if (a[j - 1] > a[j])
Swap(a[j - 1], a[j]);
}
void BubbleSortTest()
{
int n = 8;
int a[n] = {44,55,22,33,99,11,66,77};
bubblesort(a,n);
for(int i =0;i<n;i++)
{
cout<<a[i]<<endl;
}
}
3.归并排序
原理
分治的思想
将数组一直对半分块,直到分到每块内只有一个数字或者两个数字,
- 当只有一个数字时,不需要排序。
- 当块内有两个数字时,根据两个数字的大小对原数组排序。在最小块内排序完成后,将拍完虚的小块合并,合并规则,使用和原数组等大小的新数组,两个块内哪个当前数字小,保留哪个,并在响应的块内往后递增索引。合并完成后,将新数组内已排序的值赋值给原数组,继续对原数组合并。
效率分析
平均时间复杂度:O(nlogn)
最佳时间复杂度:O(n)
最差时间复杂度:O(nlogn)
空间复杂度:O(n)
排序方式:In-place
稳定性:稳定
不管元素在什么情况下都要做这些步骤,所以花销的时间是不变的,所以该算法的最优时间复杂度和最差时间复杂度及平均时间复杂度都是一样的为:O( nlogn )
归并的空间复杂度就是那个临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n)。
时间复杂度分析>>
实现
void merge(vector<int>&nums,int low,int high,vector<int>&result)
{
int resIndex = low;
int leftIndex = low;
int rightIndex =(low+high)/2+1;;
while (leftIndex <(low+high)/2+1 &&rightIndex <=high)
{
if (nums[leftIndex] <nums[rightIndex])
{
result[resIndex] = nums[leftIndex];
leftIndex ++;
}
else
{
result[resIndex] = nums[rightIndex];
rightIndex ++;
}
resIndex ++;
}
while (leftIndex < (low+high)/2+1)
{
result[resIndex] = nums[leftIndex];
resIndex ++;
leftIndex ++;
}
while (rightIndex <=high)
{
result[resIndex] = nums[rightIndex];
resIndex ++;
rightIndex ++;
}
}
void merge_sort(vector<int>&nums,int low,int high,vector<int>&result)
{
if (high -low == 0)
return ;
if (high - low == 1)
{
if (nums[low] >nums[high])
{
int temp = nums[low];
nums[low] = nums[high];
nums[high] = temp;
}
}
else
{
merge_sort(nums,low,(low+high)/2,result);
merge_sort(nums,(low+high)/2+1,high,result);
merge(nums,low,high,result);
for (int i = low; i <= high; ++i)
{
nums[i]= result[i];
}
}
}
4.计数排序
原理
① 找出待排序的数组中最大和最小的元素
② 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
③ 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
④ 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
效率分析
平均时间复杂度:O(n + k)
最佳时间复杂度:O(n + k)
最差时间复杂度:O(n + k)
空间复杂度:O(n + k)
实现
vector<int> CounterSort(vector<int>&nums)
{
int n = nums.size();
int maxnum= 0;
int minnum = 0;
vector<int>res;
for(auto item:nums)
{
maxnum = max(maxnum,item);
minnum = min(minnum,item);
}
vector<int>counter(maxnum-minnum+1,0);
for(auto item : nums)
{
counter[item] ++;
}
for(int i = 0;i<counter.size();i++)
{
for (int j = 0; j< counter[i]; ++j)
{
res.push_back(minnum+i);
}
}
return res;
}
void CounterSortTest()
{
vector<int>ip = {6,8,8,82,5,35};
vector<int>ans = CounterSort(ip);
for(auto item:ans)
cout<<item<<endl;
}
5.选择排序
原理
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面,或者将最大值放在最后面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择,每一趟从前往后查找出无序区最小值,将最小值交换至无序区最前面的位置。
效率分析
平均时间复杂度:O(N^2)
最佳时间复杂度:O(N^2)
最差时间复杂度:O(N^2)
空间复杂度:O(1)
排序方式:In-place
稳定性:不稳定 选择排序的交换操作介于和(n-1)次之间。选择排序的比较操作为n(n-1)/2次之间。选择排序的赋值操作介于0和3(n-1)次之间。比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N = (n-1) + (n-2) +…+ 1 = n x (n-1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。5.
实现
template <class T>
void mysort(T arr[],int len)
{
for (int i = 0; i <len; ++i)
{
int max = i;
for(int j = i+1;j<len;j++)
{
if(arr[max] <arr[j])
{
max = j;
}
}
if (max !=i)
{
//交换数据
SwapT(arr[max],arr[i]);
}
}
}
6.插入排序
原理
对未排序的数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序算法的一般步骤:
1.从第一个元素开始,该元素可以认为已被排序;
2.取出下一个元素,在已经排序的元素序列中从后向前扫描;
3.如果该元素(已排序)大于新元素,将该元素移到下一个位置;
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5.将新元素插入到该位置后,重复2~5。
效率分析
最差时间复杂度:O(n^2)
最优时间复杂度:O(n)
平均时间复杂度:O(n^2)
稳定性:稳定
实现
void InsertionSort(int *a, int len)
{
for (int j=1; j<len; j++)
{
int key = a[j];
int i = j-1;
while (i>=0 && a[i]>key)
{
//理解为比key大的数字都往后挪一位
a[i+1] = a[i];
i--;
}
//将key放到满足条件的位置
a[i+1] = key;
}
7.希尔排序
原理
1.先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中,在各组内进行直接插人排序。
2.取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长串行都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
效率分析:
在最优的情况下,时间复杂度为:O(n ^ (1.3) ) (元素已经排序好顺序)
在最差的情况下,时间复杂度为:O(n ^ 2);
实现:
void ShellSort(vector<int>&nums)
{
int n = nums.size();
int h = 1;
while (h<n)
h = 3*h+1;
int j;
while (h>0)
{
for (int i = h; i <n ; ++i)
{
j = i-h;
int key = nums[i];
while (j>=0&&nums[j] >key)
{
nums[j+h] =nums[j];
j -=h;
}
nums[j+h] = key;
}
h /= 3;
}
}
8.基数排序
将所有数字通过补0的方式变成长度相同数字
依次比较个位 十位 百位。。。整定数字的顺序
原理:
基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
效率分析
效率分析
时间复杂度为:posCount * (length + length) ;其中 posCount 为数组中最大元素的最高位数;简化下得:O( k*n ) ;其中k为常数,n为元素个数;
该算法的空间复杂度就是在分配元素时,使用的桶空间;所以空间复杂度为:O(10 × length)= O (length)
实现:
#include<stdio.h>
#include<stdlib.h>
void print_array(int *array, int length)
{
int index = 0;
printf("array:\n");
for(; index < length; index++){
printf(" %d,", *(array+index));
}
printf("\n\n");
}
void getCount(int *array, int length, int *count)
{
int max, index;
for (max = *array, index = 0; index < length; index++){
if ( max < *(array + index)) max = *(array + index);
}
*count = 0;
while(max){
max = max / 10;
(*count)++;
}
}
void radixSort(int *array, int length)
{
int *tmpArray = (int*)malloc(sizeof(int)*(10));
int tmp[length];
int i, j, count, log = 1;
getCount(array, length, &count);
for (j = 0; j < count; j++){ // 循环最大位数次
for (i = 0; i < 10; i++)tmpArray[i] = 0;// 初始化数组
for (i = 0; i < length; i++) tmpArray[ (array[i] / log) % 10 ]++;// 元素值对应桶标记
for (i = 1; i <= 10; i++) tmpArray[i] += tmpArray[i-1];// 统计大于各元素的个数
for (i = length - 1; i >= 0; i--){ // 按照指定位数对元素进行排序
tmp[tmpArray[ (array[i] / log) % 10] - 1] = array[i];
tmpArray[ (array[i] / log) % 10 ]--;
}
for (i = 0; i < length; i++) array[i] = tmp[i];// 把排序好的元素放回到元素数组中
log = log * 10;
}
free(tmpArray);// 释放内存
}
int main(void)
{
//int array[] = {2, 5, 337, 24, 10000, 5, 30, 123, 3, 9, 100, 1};
int array[] = {2, 5, 3, 4, 1, 5, 0, 2, 3, 9, 1, 7, 8, 6};
int length = (sizeof(array)) / (sizeof(array[1]));
print_array(array, length);
radixSort(array, length);
print_array(array, length);
return 0;
}
9.堆排序
按照从左到右,从下到上的顺序进行调整