最近在刷题,由于排序算法在平时很常见,属于基础内容,需要清楚地了解各种排序算法的优缺点,对于排序算法需要快速地反应过来,在此做一个总结。
排序算法大致分为两种:
1)比较排序,时间复杂度在O(nlogn)~O(n^2),比如:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序
2)非比较排序,时间复杂度可以达到O(n),比如:计数排序,基数排序,桶排序
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 占用空间 | 稳定性 |
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
简单选择排序 |
O(n^2)
| O(n^2) | O(n^2) | O(1) | 不稳定 |
直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
堆排序 | O(nlog(n)) | O(nlog(n)) | O(nlog(n)) | O(1) | 不稳定 |
归并排序 快速 | O(nlog(n)) O(nlog(n)) | O(nlog(n)) O(nlog(n)) | O(nlog(n)) O(n^2) | O(n) O(log(n))~O(n) | 稳定 不稳定 |
1.冒泡排序
基本思想:两两比较相邻记录的关键字,如果反序则交换。对每一对相邻元素做同样的工作,从开始第一对到结尾最后一对,最后的元素会是最大的元素。重复上述的步骤,除了最后一次。持续每次对越来越少的元素进行上面的步骤,知道没有任何一对数字需要比较。
图解算法:
程序:
void bubblesort(int[] arr) {
if(arr==nullptr || arr.length()==0) return ;
int temp;
for(int i=0;i<arr.length();i++) {
for(int j=0;j<length-1-i;j++){
if(arr[j]>arr[j+1]) {
temp=arr[j];
arr[j]=arr[j+1] ;
arr[j+1]=temp;
}
}
}
}
冒泡排序的改进:改为定向冒泡排序,它不像冒泡排序只是从低到高,而是从低到高然后从高到低。只是说在部分有序的情况下会比冒泡排序好一点,但是都是乱序的情况下,两个算法都很low。
void CocktailSort(int a[],int n)
{
int left=0;
int right=n-1;
while(left<right){
//从低到高,将最大元素放到后面
for(int i=left;i<right;i++){
if(a[i]>a[i+1]) swap(a,i,i+1);
}
right--;
//从高到低,将最小元素放到前面
for(int i=right;i>left;i--){
if(a[i-1]>a[i]) swap(a,i-1,i);
}
}
}
2.直接插入排序
基本思想:将一个元素插入到已经排序好的有序表中,从而得到一个新的,元素数增1的有序表。需要一个临时变量存储和判断数组边界之用。
图解算法:
程序:
void InsertSort(int a[],int n )
{
for(int i=1;i<n;i++){
int key=a[i];//待排序的元素
int j=i-1;//已经排序好的数组的最后一个元素
while(j>=0 && key<a[j]){
a[j+1]=a[j];//元素不断后移,知道找到合适的位置,即key不再小于它的前一个数
j--;
}
a[j+1]=key;
}
}
3.快速排序
基本思想:先从数列中取出一个数作为基准数;分区过程,将比这个数大的数全放到它的右边,小于或者等于它的数全部放在左边,再对左右区间重复第二步,直到各区间只有一个数。快速排序是通常认为在同数量级(nlogn)的排序方法中平均性能最好的,但如果初始序列中基本有序,快排反而蜕化为冒泡排序,为了改进,通常采用"三者取中"来选取基准记录,即将排序区间的两个端点和中点三个记录关键码居中的调整为支点记录。快排是一个不稳定的排序方法。
图解算法:
程序:
void quick_sort(int s[],int L,int R)
{
if(L<R){
int i=L,j=R;
int base=s[L];//这里假设以第一个数为基准数
while(i<j){
while(i<j && s[j]>base) //从后往前找,直到找到比基准数小的数
j--;
if(i<j)
s[i++]=s[j];
while(i<j && s[i]<base)//从左往右找,直到找到比基准数大的数
i++;
if(i<j)
s[j--]=s[i];
}
s[j]=base;
quick_sort(s,L,i-1);
quick_sort(s,i+1;R);
}
}
4.堆排序
基本思想:堆排序是指利用堆这种数据结构所涉及的一种排序算法,堆是一个近似完全二叉树的结构,并同事满足堆积的性质:即子节点的键值或索引总是小于(或大于)它的父节点。堆排序可以用上一次的排序结果,不需要每次都进行n-1次的比较,复杂度为O(nlogn)。
堆:本身就是一个完全二叉树,但是需要满足一定的条件,当二叉树的每个节点都大于等于它的子节点的时候,称为大顶堆,当二叉树的每个节点都小于它的子节点的时候,称为小顶堆。将堆的内容填入一个数组之后,可以通过下标计算出每个节点的父子节点。例如,一个节点的下标为k,那么它的父节点下标为(k-1)/2,其子节点为2k+1和2k+2.
堆排序的概念:初始时把要排序的n个数的序列看做是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使它成为一个堆,将堆顶元素输出,得到n个元素中最小或最大的元素,这时堆的根节点的数最小或者最大,对剩下的n-1个元素重新调整使它成为堆,输出堆顶元素,得到n个元素中次小或者次大的元素,以此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列,这个过程成为堆排序。
堆排序主要解决两个问题:1.如何将n个待排序的数建成堆 2.输出堆顶元素后,怎样调整剩余n-1个元素,使其成为一个新堆
问题1主要步骤: 1)利用给定数组创建一个堆H[0...n-1],输出堆顶元素(最小堆为例)
2)将堆底元素送入堆顶,即以最后一个元素代替堆顶,输出堆顶元素。堆已经被破坏了,仅仅因为根节点不满足堆的性质,调整成堆 (将根节点与左右子树中较小的元素进行交换)
3)若与左子树交换,如果左子树堆被破坏,则重复方法2,对于右子树是同样的道理
4)继续对不满足堆性质的子树进行上述交换操作,直到叶子节点,堆被建成
问题2主要步骤:对初始序列建堆的过程,就是一个反复进行筛选的过程:
1)n个节点的完全二叉树,则最后一个节点是第n/2个节点的子树
2)筛选从第n/2个节点为根的子树开始,该子树成为堆
3)之后向前依次对各节点为根的子树进行筛选,使之成为堆,直到根节点。
对于无序序列:(49,38,65,97,76,13,27,49)
图解算法:
程序:
- void HeapAdjust(int H[],int s, int length)
- {
- int tmp = H[s];
- int child = 2*s+1; //左孩子结点的位置。(i+1 为当前调整结点的右孩子结点的位置)
- while (child < length) {
- if(child+1 <length && H[child]<H[child+1]) { // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点)
- ++child ;
- }
- if(H[s]<H[child]) { // 如果较大的子结点大于父结点
- H[s] = H[child]; // 那么把较大的子结点往上移动,替换它的父结点
- s = child; // 重新设置s ,即待调整的下一个结点的位置
- child = 2*s+1;
- } else { // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出
- break;
- }
- H[s] = tmp; // 当前待调整的结点放到比其大的孩子结点位置上
- }
- print(H,length);
- }
- /**
- * 初始堆进行调整
- * 将H[0..length-1]建成堆
- * 调整完之后第一个元素是序列的最小的元素
- */
- void BuildingHeap(int H[], int length)
- {
- //最后一个有孩子的节点的位置 i= (length -1) / 2
- for (int i = (length -1) / 2 ; i >= 0; --i)
- HeapAdjust(H,i,length);
- }
- /**
- * 堆排序算法
- */
- void HeapSort(int H[],int length)
- {
- //初始堆
- BuildingHeap(H, length);
- //从最后一个元素开始对序列进行调整
- for (int i = length - 1; i > 0; --i)
- {
- //交换堆顶元素H[0]和堆中最后一个元素
- int temp = H[i]; H[i] = H[0]; H[0] = temp;
- //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
- HeapAdjust(H,0,i);
- }
- }
- int main(){
- int H[10] = {3,1,5,7,2,4,9,6,10,8};
- cout<<"初始值:";
- print(H,10);
- HeapSort(H,10);
- //selectSort(a, 8);
- cout<<"结果:";
- print(H,10);
- }
5.简单选择排序
基本思想:在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换,然后在剩下的数中再找最小(或者最大)的与第2个位置的数交换,以此类推,直到第n-1个元素和第n个元素比较为止。
图解算法:
程序:
int selectMin(int a[],int n,int i){
int k=i;
for(int j=i+1;j<n;++j){
if(a[k]>a[j]) k=j;
}
return k;
}
void selectSort(int a[],int n){
int key,temp;
for(int i=0;i<n;i++){
key=selectMin(a,n,i);
if(key!=i){
temp=a[i]; a[i]=a[key]; a[key]=temp;
}
}
}
改进考虑:简单选择排序,每趟循环只能确定一个元素排序后的定位,可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从未减少排序所需的循环次数。代码如下:
void selecrSort(int r[],int n)
{
int i,j,min ,max,temp;
for(i=1;i<n/2;i++){
min=i; max=i;
for(j=i+1;j<n-i;j++){
if(r[j]>r[max]) { max=j; continue; }
if(r[j]<r[min]) min=j;
}
temp=r[i-1]; r[i-1]=r[min]; r[min]=temp;
temp=r[n-i]; r[n-i]=r[max]; r[max]=temp;
}
}
6.归并排序
归并排序分为递归实现和非递归实现。递归实现的归并排序是分治策略的典型应用,将一个大问题分成很多个小问题去解决,然后用所有小问题的答案来解决整个大问题。非递归(迭代)实现的归并排序首先进行的是两两归并,然后是四四归并,最后八八归并,以此类推,直到归并了整个数组。
基本思想:归并算法主要依赖于merge操作,指的是将两个已经排序的序列合并成一个序列的操作。
步骤如下:
1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2)设定两个指针,最初位置分别是两个已经排序序列的起始位置
3)比较两个指针所指向的元素,选择相对小的元素放到合并空间,并移动指针到下一个位置
4)重复步骤3直到某一个指针到达序列尾部
5)将另一序列剩下的所有元素直接复制到合并序列尾
图解算法:
程序:
Merge(int a[],int left,int mid,int right)
{
int len=right-left+1;
int *temp=new int[len];//辅助空间O(n)
int index=0;
int i=left; //前一数组的起始位置
int j=mid+1; //后一数组的起始位置
while(i<=mid && j<=right){
temp[index++]=a[i]<=a[j]?a[i++]:a[j++];
}
while(i<=mid){
temp[index++]=a[i++];
}
while(j<=right){
temp[index++]=a[j++];
}
for(int k=0;k<len;k++){
a[left++]=temp[k];
}
}
递归实现:
void MergeSortRecursion(int a[],int left,int right)
{
if(left==right) //递归开始回溯,进行merge操作
return ;
int mid = (left+right)/2;
MergeSortRecursion(a,left,mid);
MergeSortRecursion(a,mid+1,right);
Merge(a,left,mid,right);
}