您的位置:首页 > 理论基础 > 数据结构算法

【数据结构排序算法系列】数据结构八大排序算法

2016-03-25 22:36 267 查看
排序算法在计算机应用中随处可见,如Windows操作系统的文件管理中会自动对用户创建的文件按照一定的规则排序(这个规则用户可以自定义,默认按照文件名排序)因此熟练掌握各种排序算法是非常重要的,本博客将对数据结构中常见的八大排序算法进行详细的讲解。
写在前面的话:首先要明白在学习同类型的排序算法时,一定要明白任何相同领域存在的东西一定是竞争之后的结果,所以对于相同类型的排序之所以能够存在多种被人熟知,一定是因为它们各自存在各自的长处,可能是时间复杂度也可能是空间复杂度,也可能是因为稳定性。就像C,C++,Java它们之所以能同时存在,是因为在不同的领域它们的效率不同,所以学习排序算法时一定要知道同类型的排序的各自特点,如快速排序之于冒泡排序。

一.排序算法的归类:

总的排序算法分为以下几类:
1.插入类排序:如:直接插入排序,折半插入排序,希尔排序

2.交换类排序:如:冒泡排序,快速排序,其中冒泡排序应该是计算机专业的学生最为熟悉的一种排序算法了,而快速排序则是在冒泡排序的基础上一次消除多个逆序对改进而来
3.选择类排序:如:简单选择排序,堆排序。

4.其它类排序:如:归并排序,基数排序等。

二 各大排序的特点,如:时空复杂度,稳定性。



三各种排序详解
1交换类排序:
1.1冒泡排序:冒泡排序是通过对相邻的数据元素进行交换,逐步将待排序序列排成有序序列的过程。
如以升序为例(假设存储结构为数组array[len],长度为len):在一趟冒泡排序中,从第一个记录开始,扫描整个待排序序列(注意是待排序序列,而不是整个记录序列,待排序序列随着排序的趟数的增加而减少,最后一趟待排序序列为2,只用交换两个元素),在一趟扫描中,最终必然将最大的元素排在待排序序列的末尾,这也是最大元素应该在的位置,第一躺时会将整个记录中最大元素排在最后一个位置array[len]。
然后进行第二趟,重复上述过程,结果将次大记录放在第array[len-1]上,......重复上述过程,直至整个数组余下一个记录为止。
若在某一趟的冒泡排序过程中,一个逆序也没找到,则可以直接结束整个排序过程,所以冒泡排序过程最多只进行len-1趟,冒泡排序也是唯一一个可以不用排序而直接终止排序的排序算法。
冒泡排序的代码如下:(采用C++模板类)

#include<iostream>
using namespace std;
template<typename T>
void bubble( T t[],int len)//注意模板中的参数T为参数类型,所以不能写成T[]
{
bool flag=true;
int i,j;
for(i=1;i<=len-1&&flag;i++)
{
flag=false;
for(j=0;j<len-i;j++)//如果外层循环是从1开始,那么内层循环j<len-i,
//不能取等号,否则会产生下标越界,因为下面的交换判断语句为t[j]
//与t[j+1],
{
if(t[j]>t[j+1])
{
swap(t[j],t[j+1]);
flag=true;
}
}
}
}

void main()
{
int a[]={4,2,1,3,5,7,6};
char x[100]={'x','y','s','a','n','c','m'};//此处必须指定数组的大小,虽然不指定也不会出错,但是当用字符去初始化一个没定义长度的字符数组时,系统不会默认在
//末尾添加'\0',所以此时不能用strlen函数来求该字符数组的长度,而当指定数组的大小后,余下的系统自动赋值为空,即'\0'

//	int len=strlen(a);错误,strlen函数的参数为char*类型
int len=sizeof(a)/4;
for(int i=0;i<len;i++)
cout<<a[i]<<' ';
cout<<endl;
cout<<"排序后结果为"<<endl;
bubble(a,len);
for(int i=0;i<len;i++)
cout<<a[i]<<' ';
cout<<endl;

int str_len=strlen(x);
for(int i=0;i<len;i++)
cout<<x[i]<<' ';
cout<<endl;
cout<<"排序后结果为"<<endl;
bubble(x,str_len);
for(int i=0;i<len;i++)
cout<<x[i]<<' ';
cout<<endl;

}
复杂度分析:冒泡排序最好的情况就是当待排序序列正序排列的时候,则只需要进行一趟排序,在排序过程中进行n-1次比较,而不需要移动记录,即外层for循环只需执行一次,此时复杂度为O(n).最坏的情况就是逆序排列的时候,则第i趟需要进行n-i次比较,3(n-i)次移动,即外层for循环与内层for循环都得执行,总的比较次数为n(n-1)/2,而移动次数为3n(n-1)/2,此时复杂度为O(n*n).



1.2快速排序:快速排序作为和冒泡排序相同类型的排序(同为交换类排序),之所以能够被人们所熟知,是因为它解决了冒泡排序只用来对相邻两个元素进行比较,因此在互换两个相邻元素时只能消除一个逆序,而快速排序是通过两个不相邻元素的交换,来消除待排序记录中的多个逆序。即快速排序中的一趟交换可以消除多个逆序。
具体思想:从待排序记录中选取一个记录(通常选取第一个记录,当然也可采用随即划分的方式,这样能大大提高快速排序的执行效率,如我们所熟知的在O(n)的时间复杂度内找出前k元的算法),将其关键字记为K1,然后将其余关键字小于K1的记录移到前面,而大于关键字K1的移到后面,一趟快速排序之后,将待排序序列划分为两个子表,最后将关键字K1插到这两个子表的分界线的位置。具体实现就是用三层while循环,最外层的while循环控制该趟快速是否进行,而内层的两个while循环一个用来从左到右扫描大于该趟基准记录关键字的元素,一个用来从右到左扫描小于该趟基准记录的关键字的元素,可以用两个指针low和high指向当前从左到右和从右到左扫描的当前记录的关键字,找到一个就将其交换。
以上是一趟快速排序的思想,对上述划分后的子表重复上述过程,直至划分后的子表的长度不超过1为止。即为快速排序的思想,从定义可知快速排序是一个递归排序。
具体代码如下:

#include<iostream>
using namespace std;
const int len=7;
int qk(int a[],int low,int high)//注意快速排序函数的参数,因为每一趟快速排序的作用是要将数组分割为两个部分,前面一部分不大于K,后面一部分不小于K,然后在                                //对前一部分和后一部分继续进行快速排序,所以,qk的第二个参数与第三个参数应为low与high
{
int x=a[low];//选取第一个元素作为基准记录
while(low<high)
{
while(low<high&&a[high]>=x)
high--;
if(low<high)
{
a[low]=a[high];
low++;
}
while(low<high&&a[low]<=x)
low++;
if(low<high)
{
a[high]=a[low];
high--;
}
}
a[low]=x;
return low;
}
void qsort(int a[],int low,int high)
{
if(low<high)
{
int pos=qk(a,low,high);
qsort(a,low,pos-1);
qsort(a,pos+1,high);
}
}
void main()
{
int a[len]={7,4,5,1,2,3,6};
cout<<"快速排序后的结果为:"<<endl;
qsort(a,0,len-1);
for(int i=0;i<len;i++)
{
cout<<a[i]<<' ';
}
cout<<endl;
}
复杂度分析:快速排序最好的情况就是每一趟排序将序列划分为两个部分,正好在表中间将表划分为两个大小相等的子表,类似于折半查找,此时复杂度为O(nlog2n).

最坏的情况就是已经排好序,则第一趟经过n-1次比较,第一个记录定在原位置,左部子表为空表,右部子表为n-1个记录,第二趟n-1个记录经过n-2次比较,第二个记录定在原位置.....即此时快速排序内层中的两个while循环不执行,此时快速排序退化为冒泡排序,总的比较次数为n*(n-1)/2,复杂度为O(n*n).

程序运行结果如下:



2 选择类排序:选择类排序的基本思想就是在每一趟的n-i+1(i=1,2,.....,n-1)各记录中选取关键字最小的记录作为有序序列中的第i个记录,即选择类排序的有序序列位于前面,而上面讲到的交换类排序,如冒泡排序很显然有序序列在后半部分。
2.1简单选择排序:就是每趟扫描整个记录找到当前待排序序列中最小的(以升序为例),与第i(i=1,2,.....,n-1)个记录交换,很显然需要n-1趟,余下的最后一个元素直接在最后。
2.2堆排序:堆排序是利用堆这个数据结构(逻辑上用完全二叉树表示,物理上用数组表示),堆排序之所以被重视,主要是用在海量数据处理的TOP K问题中,如百度热词,从百度海量的用户搜索记录中查找被搜索次数最多的前K个关键词,这样我们只需要维护一个K元的堆即可,然后对海量数据分批加载到内存进行堆排序,最后所有数据都访问过后堆中的内容即为海量数据中TOP K元素。
关于堆排序的具体算法,请参看我的博客:数据结构排序算法之堆排序

3其它类排序:
3.1归并排序:归并排序实际上就是将几个已排好序的子表合为一张新的有序表。
基于C++模板类的归并排序代码如下:

#include<iostream>
using namespace std;
template<typename T>
void merge(T t[],int low,int mid,int high)//mid参数表示t[low]-t[mid]和t[mid+1]-t[high]是按关键字排好序的
{
int i=low,j=mid+1;
int *temp=new int[high-low+1];
int k=0;
while(i<=mid&&j<=high)
{
if(t[i]<t[j])
{
temp[k]=t[i];
k++;
i++;
}
else
{
temp[k]=t[j];
k++;
j++;
}
}
while(i<=mid)
{
temp[k++]=t[i++];
}
while(j<=high)
{
temp[k++]=t[j++];
}
for(int i=low,j=0;i<high,j<k;i++)
{
t[i]=temp[j++];
}
}

template<typename T>
void mergeSort(T t[], int low, int high)
{
if(low < high-1)
{
int mid = (low+high)/2;
mergeSort(t,low,mid);	//前半部分排序
mergeSort(t,mid+1,high);	//后半部分排序
merge(t,low,mid,high);
}
}
void main()
{
int a[]={1,3,4,6,2,5,7};
mergeSort(a,0,6);
for(int i=0;i<7;i++)
{
cout<<a[i]<<' ';
}
cout<<endl;
}

程序运行结果如下:



4插入类排序:
4.1希尔排序:即缩小增量排序,顾名思义就是将待排序序列划分为若干个“较稀疏的”子序列(通过多次调用希尔排序,直至最终增量为1为止,即缩小增量),它基于插入排序的思想,利用了直接插入排序的最佳性质(len较小),将这若干个子序列进行直接插入排序。
代码如下:

#include <iostream>
using namespace std;
const int len=7;
void ShellInsert(int a[],int len,int delta)
{
for(int i = delta; i<len; i++)
{
if(a[i] < a[i-delta])
{
int key = a[i];
int j;
for(j = i-delta;j>=0 && key < a[j]; j-=delta)
{
a[j+delta] = a[j];
}
a[j+delta] = key;
}
}
}

void ShellSort(int a[], int len, int delta[] , int n)
{
for(int i=0;i<n;i++)
{
ShellInsert(a,len,delta[i]);
}
}

int main()
{
int a[len]={7,4,5,1,2,3,6};
int delta[len] = {5,2,1};
ShellSort(a,len,delta,3);

for(int i=0; i<len; i++)
{
cout << a[i] << " ";
}
cout << endl;
}

程序运行结果如下:



四.排序算法中的一些概念:
稳定性:排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序和排序之前相同,则称该算法是稳定的;若排序后,记录的相对次序和排序前不同,则该算法是不稳定的。

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序

当待排序序列顺序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当待排序序列基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
待排序序列是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。

以上就是本人理解的关于数据结构排序算法的相关内容,重点掌握冒泡排序,快速排序,堆排序以及它们各自的特点。

觉得不错的话,随手点击一下下面的广告支持支持我吧!

点击打开连接
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: