常用的有十种排序算法,包含了插入、选择、交换、分治、线性五种类别,本篇博客将对这十种排序算法做一个总结,并附带C++代码
在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
void InserttSort(int array[], int length)
{
for(int i = 0; i < length - 1; ++ i)
{
for(int j = i + 1; j > 0; --j)
{
if(array[j] < array[j - 1])
MySwap(array, j, j - 1);
else
break;
}
}
}
插入排序一种高效率的实现,也叫缩小增量排序
简单插入排序中,如果序列是基本有序的,使用直接插入排序效率就非常高
希尔排序利用了这个特点:先将整个序列分割成若干个子序列进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序
注意:分割子序列的时候不是逐段分割,而是将某个相隔增量的元素组成一个子序列
较小的元素跳跃式往前挪动,比直接插入排序效率高
时间复杂度需要复杂的数学推算
void ShellSort(int array[], int length)
{
int incre = length;
while (true)
{
incre = incre / 2;
cout << "incre: " << incre << endl;
for(int k = 0; k < incre; ++k)//根据增量分为若干子列
{
for(int i = k + incre; i < length ; i += incre)
{
for(int j = i; j > k; j -= incre)
{
if(array[j] < array[j - incre])
MySwap(array, j, j - incre);
else
break;
}
}
}
if(incre == 1)
break;
}
}
依次选择最小、第二小。。。的数放在第一位、第二位。。。
第一次遍历n-1个数,第二次n-2个数
void SelectSort(int array[], int length)
{
for(int i = 0; i < length -1; ++i)
{
int minIndex = i;
for(int j = i + 1; j < length; ++j)
{
if(array[j] < array[minIndex])
minIndex = j;
}
if(minIndex != i)
MySwap(array, minIndex, i);
}
}
利用堆这种数据结构而设计的一种排序算法,是一种选择排序
最坏、最好、平均时间复杂度均为O(nlogn)
堆
是具有以下性质的完全二叉树:
每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
映射到数组中
所以大顶堆:arr[i] >= arr[2i + 1] && arr[i] >= arr[2i + 2]
小顶堆:arr[i] <= arr[2i + 1] && arr[i] <= arr[2i + 2]
堆排序思路:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
步骤
总结基本思路:
void AdjustHeapNode(int array[], int i, int length)//调整大顶堆
{
int k = i * 2 + 1;
while(k < length)
{
if(k + 1 < length && array[k] < array[k + 1])//如果左子节点小于右子节点,k指向右子节点
++k;
if(array[k] > array[i])//如果子节点大于父节点,将子节点赋值给父节点(不用进行交换)
{
MySwap(array, i, k);
}
else
break;
i = k; //检查更换的节点是否满足最大堆的特性
k = 2 * i + 1;
}
}
void HeapSort(int array[], int length)
{
//构建大顶堆
for(int i = (length - 1) / 2; i >= 0; i--) //最后一个非叶子节点开始
{
cout << i << " ";
AdjustHeapNode(array, i, length);
}
cout << endl;
//调整堆结构+交换堆顶元素与末尾元素
for(int j = length - 1; j > 0; j--)
{
MySwap(array, 0, j);
AdjustHeapNode(array, 0, j);
}
}
两个数比较大小,较大的下沉,较小的数冒起来
从最后开始冒,总共走length-1趟,每次排好一个数
void BubbleSort(int array[], int length)
{
if(array == nullptr || length <= 0)
return;
for(int i = 0; i < length - 1; ++i)
{
for(int j = length - 1; j > i; --j)
if(array[j] < array[j - 1])
MySwap(array, j, j-1);
}
}
优化
因为冒泡排序可能出现已经排好序但是依然要走满length-1趟的情况
添加一个标志,每趟开始前置零,当发生交换置1
每趟跑完检测是否为0,如果为0,说明本次没有发生交换,已经排好序了
void BubbleSort_Better(int array[], int length)
{
if(array == nullptr || length <= 0)
return;
for(int i = 0; i < length - 1; ++i)
{
bool flag = false;
for(int j = length - 1; j > i; --j)
if(array[j] < array[j - 1])
{
MySwap(array, j, j-1);
flag = true;
}
if(!flag) break;
}
}
基本思路(分治)
key值的选取有多种形式,例如中间数或者随机数
快排的时间性能取决于快排递归的深度
最优情况就是每次都恰好把数组分成两半,最优时间复杂度为O(nlogn)
最坏情况就是正序或者逆序(恰好与所要求的的排序相反),最差时间负责度是O(n^2)
时间复杂度和空间复杂度推导:可以看看这篇文章 文章地址
void QuickSort(int array[], int left, int right)
{
if(left >= right)
return;
int i = left, j = right, key = array[left];
while(i < j)
{
while(i < j && array[j] >= key)//从右开始找第一个小于等于key的值
j--;
if(i < j)
{
array[i] = array[j];
i++;
}
while(i < j && array[i] < key)
i++;
if(i < j)
{
array[j] = array[i];
j--;
}
}
array[i] = key;
QuickSort(array, left, right - 1);
QuickSort(array, left + 1, right);
}
建立在归并操作上的一种有效的排序算法
分治法的典型应用
首先要考虑如何将两个有序数列合并(有序数列合并问题,三个指针)
归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。
空间复杂度为O(n)
void mergeArray(int array[], int first, int middle, int last)
{
int temp[last - first +1];
int i = first;
int m = middle;
int j = middle + 1;
int n = last;
int k = 0;
while(i <= m && j <= n)
{
if(array[i] <= array[j])
{
temp[k] = array[i];
k++;
i++;
}
else
{
temp[k] = array[j];
k++;
j++;
}
}
while(i <= m)
{
temp[k] = array[i];
k++;
i++;
}
while(j <= n)
{
temp[k] = array[j];
k++;
j++;
}
for(int ii = 0; ii < k; ii++)
array[first+ii] = temp[ii];
}
void MergeSort(int array[], int first, int last)
{
if(first < last)
{
int middle = (first + last) / 2;
MergeSort(array, first, middle);
MergeSort(array, middle + 1, last);
mergeArray(array, first, middle, last);
}
}
时间复杂度为O(n)的排序算法
适用于待排序数有范围,需要较多的辅助空间,空间大小与待排序数范围的大小有关
设定一个计数数组(大小为待排序数最大值加1)
将待排序数读入并给对应值自增
输出到待排序数组完成排序
int get_max(int array[], int length)//获取数组中最大值的函数
{
if(array == nullptr || length <= 0)
return -1;
int max = array[0];
for(int i = 1; i < length; i++)
{
if(array[i] > max)
max = array[i];
}
return max;
}
void CountSort(int array[], int length)
{
if(array == nullptr || length <= 0)
return;
int max = get_max(array, length) + 1;//获取数组中的最大值
cout << max << endl;
int *count= new int[max];//分配空间
for(int i = 0; i < max; i++)//初始化计数数组各位为0
count[i] = 0;
// for(int i = 0; i < max; i++)
// cout << count[i] << " ";
// cout <
for(int i = 0; i < length; i++)//计数
{
count[array[i]]++;
}
for(int i = 0; i < max; i++)
cout << count[i] << " ";
cout <<endl;
for(int i = 0, j = 0; i < max; i++)//输出到源数组,完成排序
{
for(int k = count[i]; k > 0; k--)
{
array[j] = i;
j++;
}
}
}
是对计数排序的一种改进和推广
全依赖“比较”操作的排序算法时间复杂度的一个下界O(N*logN)
这些算法并不是不用“比较”操作,也不是想办法将比较操作的次数减少到 logN。而是利用对待排数据的某些限定性假设 ,来避免绝大多数的“比较”操作。
基本思想
映射函数
使用映射函数,减少了几乎所有的比较工作
对N个关键字进行桶排序的时间复杂度分为两个部分:
很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到**O(N*logN)**了)。因此,我们需要尽量做到下面两点:
对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M*(N/M)log(N/M))=O(N+N(logN-logM))=O(N+NlogN-NlogM)
当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。
总结
int DataMap(int num) //桶排序映射函数
{
return num / 10;
}
void BucketSort(int array[], int length)
{
if(array == nullptr || length <= 0)
return;
//vector> bucket;//使用双向链表来存储桶内元素,同vector来组织桶
list<int> bucket[10];//使用双向链表来存储桶内元素,用数组来组织桶
for(int i = 0; i < length; i++)
{
bucket[DataMap(array[i])].push_back(array[i]);//给对应的桶中插入,插入操作O(1)时间复杂度
}
for(int i = 0; i < 10; i++)//分别对每个桶中的元素进行排序
{
bucket[i].sort();
}
for(int i = 0, k = 0; i < 10; i++)//输出桶中的元素到序列中,完成排序
{
for(auto j : bucket[i])
{
if(k < length)
{
array[k] = j;
k++;
}
}
}
}
BinSort想法非常简单,首先创建数组A[MaxValue];然后将每个数放到相应的位置上(例如17放在下标17的数组位置);最后遍历数组,即为排序后的结果。
问题: 当序列中存在较大值时,BinSort 的排序方法会浪费大量的空间开销。
基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。
所谓的多关键字排序就是有多个优先级不同的关键字。
如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。
void BitCountSort(int array[], int length, int exp)//按位计数排序函数
{
int range[10];
int temparr[length];
for(int i = 0; i <10; i++)
range[i] = 0;
for(int i = 0; i < length; i++)
{
range[(array[i]/exp)%10]++;
}
cout << "range :" << endl;
for(int i = 0; i < 10; i++)
{
cout << range[i] << " ";
}
cout << endl;
for(int i = 1; i < 10; i++)
{
range[i] += range[i-1];//统计本应出现的位置
}
cout << "range :" << endl;
for(int i = 0; i < 10; i++)
{
cout << range[i] << " ";
}
cout << endl;
for(int i = length - 1; i >=0; i--)
{
temparr[range[(array[i]/exp)%10] - 1] = array[i];
range[(array[i]/exp)%10]--;
}
for(int i = 0; i < length; i++)
{
array[i] = temparr[i];
}
}
void RadixSort(int array[], int length)
{
int max = -1;
//提取最大值
for(int i = 0; i < length; i++)
{
if(array[i] > max)
max = array[i];
}
//提取每一位进行比较位数不足的高位补0
for(int exp = 1; max/exp > 0; exp *= 10)
BitCountSort(array, length, exp);
}
简单排序:冒泡排序、选择排序、插入排序
简单排序的变种:快速排序、堆排序、希尔排序
基于分治递归的:归并排序
线性排序:计数排序、桶排序、基数排序
我们希望如果关键值相等的时候,先输入的数据应该还是排在前面,而不是随便排