排序小结
第一部分复杂度为O(n2)。
冒泡法、交换法、直接选择法、双端选择法、插入法
第二部分复杂度为O(n1.5)
第三部分复杂度为O(n)。
箱排序、基数排序
一、简单排序算法
1.交换法:
最简单直接的排序法,重复扫描第i个元素后面的元素,只要比他小就交换位置。
template<typename T>
void ExchangeSort(vector<T>& V)
{
for (int i=0;i<V.size();i++)
{
for (int j=i+1;j<V.size();j++)
{
if (V[i]>V[j])
{
swap(V[i],V[j]);
}
}
}
最好情况:顺序,此时进行(n-1)*n/2次循环,(n-1)*n/2次比较,但是不进行交换。
最坏情况:逆序,此时进行(n-1)*n/2次循环,(n-1)*n/2次比较,(n-1)*n/2次交换。
算法复杂度:只根据循环来比较的话为O(n*n)。
2.冒泡法:
假设待排序的是一组重量不同的气泡,根据轻气泡不能在重气泡之下的原则进行排序,凡扫描到违反本原则的轻气泡,就使其向上"飘浮"。如此反复进行,直到最后任何两个气泡都是轻者在上,重者在下为止。
template<typename T>
void BubbleSort(vector<T>& V)
{
bool bExchanged;
for (int i=0;i<V.size();i++)
{
bExchanged=false;
for (int j=i+1;j<V.size();j++)
{
if (V[i]>V[j])
{
swap(V[i],V[j]);
bExchanged=true;
}
}
if (!bExchanged)
{
break;
}
}
此算法与交换法几乎一致,除了多了一个标志表明此次扫描是否进行了交换,如果本次没有进行交换的话,说明此算法可以提前结束,不需要继续下一次循环。因此,这个算法有可能比交换法好一点(剩余的部分是有序的时候)。
3.直接选择法:
从剩余的数据中选择最小的放在已经排好的序列的队尾。
template<typename T>
void SelectionSort(vector<T>& V)
{
if (V.size()<2)
return;
if (V.size()==2)
{
if (V[0]>V[1])
swap(V[0],V[1]);
return;
}
int iPos;
for (int i=0;i<V.size();i++)
{
iPos=i;
for (int j=i+1;j<V.size();j++)
{
if (V[iPos]>V[j])
iPos=j;
}
if (iPos!=i)
{
swap(V[i],V[iPos]);
}
}
算法需要的循环次数依然是(n-1)*n/2,所以算法复杂度为O(n*n)。
看看他的交换:由于每次外层循环只产生一次交换(只有一个最小值),所以f(n)<=n
因此交换的复杂度是O(n)。所以,在数据较乱的时候,可以减少一定的交换次数。
4.双端选择排序
由直接选择排序变种而来,每次定位每个子表中的最小和最大元素,并把它们分别放在子表的开头和结尾。
template<typename T>
void BiSelectionSort(vector<T>& V)
{
if (V.size()<2)
return;
if (V.size()==2)
{
if (V[0]>V[1])
swap(V[0],V[1]);
return;
}
int iPosMax,iPosMin;
int subTableSize=V.size();
if (V[0]>V[subTableSize-1])
{
swap(V[0],V[subTableSize-1]);
}
for (int i=0;i<subTableSize;i++)
{
iPosMin=i;
iPosMax=subTableSize-1;
for (int j=i+1;j<subTableSize-1;j++)
{
if (V[iPosMax]<V[j])
iPosMax=j;
if (V[iPosMin]>V[j])
iPosMin=j;
}
if (iPosMin!=i)
swap(V[i],V[iPosMin]);
if (iPosMax!=subTableSize-1)
swap(V[subTableSize-1],V[iPosMax]);
subTableSize--;
}
}
直接选择排序需要进行n次外循环,而这种排序只进行n/2次外循环,但是交换的次数并不比他少多少,因此,这种算法比起直接选择排序要少一些时间但是并没有快一倍。它的算法复杂度依然为O(n*n)。
5.插入法:
起源:老师在发回试卷之前,经常要把试卷按分数从低到高进行排列。第一次假设第一张卷子的位置是正确的,从第二张卷子开始,将它插入到已经排好顺序的那一堆卷子里面,直到全部卷子排列完毕。
template<typename T>
void InsertSort(vector<T>& V)
{
if (V.size()<2)
return;
if (V.size()==2)
{
if (V[0]>V[1])
swap(V[0],V[1]);
return;
}
vector<T>::iterator itI,itJ,itPos;
T temp;
for (itI=V.begin();itI!=V.end()-1;itI++)
{
itPos=itI+1;
temp=*itPos;
for (itJ=V.begin();itJ!=itI+1;itJ++)//内循环代表已经排好的卷子
{
if (temp<*itJ)//因为只要发现手上的这一张比正在扫描的有序卷子的这一张分数低,
{ //同时他肯定比前一张扫描的卷子分数高,所以能够确定它的位置,可以结束扫描
itPos=itJ;
break;
}
}
if (itPos!=itI+1)
{
V.erase(itI+1);
V.insert(itPos,temp);
}
}
如果是顺序,循环次数是n*(n-1)/2次,没有交换;如果是倒序,循环次数就是n次,因为内循环只执行一次就中断了,交换也是n次,所以,算法复杂度在逆序的时候是O(n),在顺序的时候是O(n*n),平均来说应该是简单排序里面最好的。
(10000个元素) | 顺序 | 逆序 | 随机 |
交换法 | 11.531 | 22.187 | 15.484 |
冒泡法 | 0 | 22.063 | 15.453 |
直接选择法 | 11.453 | 11.156 | 11.141 |
双端选择法 | 9.203 | 8.922 | 8.906 |
插入法 | 0.172 | 0.875 | 0.829 |
根据上面的数据,在简单排序算法里面,综合来说插入法是最好的;顺序的时候,冒泡法在第一次扫描的时候就直到这个序列不需要进行排序,因此确定了终止循环,这时他是最好的算法;还可以发现直接选择法和双端选择法是比较稳定的算法,基本上不受数列的乱的程度的影响;
二、高级排序算法:
高级排序算法中我们将只介绍这一种,同时也是目前我所知道(我看过的资料中)的最快的。它的工作看起来仍然象一个二叉树。首先我们选择一个中间值middle程序中我们使用数组中间值,然后把比它小的放在左边,大的放在右边(具体的实现是从两边找,找到一对后交换)。然后对两边分别使用这个过程(最容易的方法——递归)。
1.快速排序:
#include <iostream.h>
void run(int* pData,int left,int right)
{
int i,j;
int middle,iTemp;
i = left;
j = right;
middle = pData[(left+right)/2]; //求中间值
do{
while((pData[i]<middle) && (i<right))//从左扫描大于中值的数
i++;
while((pData[j]>middle) && (j>left))//从右扫描大于中值的数
j--;
if(i<=j)//找到了一对值
{
//交换
iTemp = pData[i];
pData[i] = pData[j];
pData[j] = iTemp;
i++;
j--;
}
}while(i<=j);//如果两边扫描的下标交错,就停止(完成一次)
//当左边部分有值(left<j),递归左半边
if(left<j)
run(pData,left,j);
//当右边部分有值(right>i),递归右半边
if(right>i)
run(pData,i,right);
}
void QuickSort(int* pData,int Count)
{
run(pData,0,Count-1);
}
void main()
{
int data[] = {10,9,8,7,6,5,4};
QuickSort(data,7);
for (int i=0;i<7;i++)
cout<<data[i]<<" ";
cout<<"/n";
}
这里我没有给出行为的分析,因为这个很简单,我们直接来分析算法:首先我们考虑最理想的情况
1.数组的大小是2的幂,这样分下去始终可以被2整除。假设为2的k次方,即k=log2(n)。
2.每次我们选择的值刚好是中间值,这样,数组才可以被等分。
第一层递归,循环n次,第二层循环2*(n/2)......
所以共有n+2(n/2)+4(n/4)+...+n*(n/n) = n+n+n+...+n=k*n=log2(n)*n
所以算法复杂度为O(log2(n)*n)
其他的情况只会比这种情况差,最差的情况是每次选择到的middle都是最小值或最大值,那么他将变成交换法(由于使用了递归,情况更糟)。但是你认为这种情况发生的几率有多大??呵呵,你完全不必担心这个问题。实践证明,大多数的情况,快速排序总是最好的。
如果你担心这个问题,你可以使用堆排序,这是一种稳定的O(log2(n)*n)算法,但是通常情况下速度要慢于快速排序(因为要重组堆)。
1.双向冒泡:
通常的冒泡是单向的,而这里是双向的,也就是说还要进行反向的工作。
代码看起来复杂,仔细理一下就明白了,是一个来回震荡的方式。
写这段代码的作者认为这样可以在冒泡的基础上减少一些交换(我不这么认为,也许我错了)。
反正我认为这是一段有趣的代码,值得一看。
#include <iostream.h>
void Bubble2Sort(int* pData,int Count)
{
int iTemp;
int left = 1;
int right =Count -1;
int t;
do
{
//正向的部分
for(int i=right;i>=left;i--)
{
if(pData[i]<pData[i-1])
{
iTemp = pData[i];
pData[i] = pData[i-1];
pData[i-1] = iTemp;
t = i;
}
}
left = t+1;
//反向的部分
for(i=left;i<right+1;i++)
{
if(pData[i]<pData[i-1])
{
iTemp = pData[i];
pData[i] = pData[i-1];
pData[i-1] = iTemp;
t = i;
}
}
right = t-1;
}while(left<=right);
}
void main()
{
int data[] = {10,9,8,7,6,5,4};
Bubble2Sort(data,7);
for (int i=0;i<7;i++)
cout<<data[i]<<" ";
cout<<"/n";
}
2.希尔排序
希尔排序(Shell Sort)也称为递减增量排序算法,是插入排序的一种高速而安定的改良版。因希尔(Donald L. Shell)于1959年提出而得名。各种实现在如何进行递减上有所不同。该方法实质上是一种分组插入方法。
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
书上说有效序列是1,4,13,40,121,364,...,使用for(d1=1;d1<n/9;d1=3*d1+1)来获得d1的值。
template<typename T>
void ShellSort(vector<T>& V)
{
if (V.size()<2)
return;
if (V.size()==2)
{
if (V[0]>V[1])
swap(V[0],V[1]);
return;
}
int d1=V.size();
d1=d1/3+1;//这一句不要效率也差不多
T temp;
while (d1>1)
{
d1=d1/3+1;
for (int i=d1;i<V.size();i++)//for2
{
if(V[i]<V[i-d1])
{
temp=V[i];
int j=i-d1;
while (j>=0&&temp<V[j])
{
V[j + d1] = V[j];
j = j - d1;
}
V[j + d1] =temp;
}
}
}
Shell排序的时间性能优于直接插入排序
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插人排序有较大的改进。
希尔排序是不稳定的.
三、复杂度为O(n)的排序算法
1、箱排序(Bin Sort)
箱排序也称桶排序(Bucket Sort),其基本思想是:设置若干个箱子,依次扫描待排序的记录R[0],R[1],…,R[n-1],把关键字等于k的记录全都装入到第k个箱子里(分配),然后按序号依次将各非空的箱子首尾连接起来(收集)。
若R[0..n-1]中关键字的取值范围是0到m-1的整数,则必须设置m个箱子。因此箱排序要求关键字的类型是有限类型,否则可能要无限个箱子。
一般情况下每个箱子中存放多少个关键字相同的记录是无法预料的,故箱子的类型应设计成链表为宜。
(1) 实现方法一
每个箱子设为一个链队列。当一记录装入某箱子时,应做人队操作将其插入该箱子尾部;而收集过程则是对箱子做出队操作,依次将出队的记录放到输出序列中。
(2) 实现方法二
若输入的待排序记录是以链表形式给出时,出队操作可简化为是将整个箱子链表链接到输出链表的尾部。这只需要修改输出链表的尾结点中的指针域,令其指向箱子链表的头,然后修改输出链表的尾指针,令其指向箱子链表的尾即可。
分配过程的时间是O(n);收集过程的时间为O(m) (采用链表来存储输入的待排序记录)或O(m+n)。因此,箱排序的时间为O(m+n)。若箱子个数m的数量级为O(n),则箱排序的时间是线性的,即O(n)。
注意:
箱排序实用价值不大,仅适用于作为基数排序的一个中间步骤。
2.基数排序(Radix Sort)
基数排序(Radix Sort)是对箱排序的改进和推广。
(1)单关键字和多关键字
文件中任一记录R[i]的关键字均由d个分量
构成。
若这d个分量中每个分量都是一个独立的关键字,则文件是多关键字的(如扑克牌有两个关键字:点数和花色);否则文件是单关键字的,
(0≤j<d)只不过是关键字中其中的一位(如字符串、十进制整数等)。
多关键字中的每个关键字的取值范围一般不同。如扑克牌的花色取值只有4种,而点数则有13种。单关键字中的每位一般取值范围相同。
(2)基数
设单关键字的每个分量的取值范围均是:
C0≤kj≤Crd-1(0≤j<d)
可能的取值个数rd称为基数。
基数的选择和关键字的分解因关键宇的类型而异:
(1) 若关键字是十进制整数,则按个、十等位进行分解,基数rd=10,C0=0,C9=9,d为最长整数的位数;
(2) 若关键字是小写的英文字符串,则rd=26,Co='a',C25='z',d为字符串的最大长度。
(3)基数排序的基本思想
基数排序的基本思想是:从低位到高位依次对Kj(j=d-1,d-2,…,0)进行箱排序。在d趟箱排序中,所需的箱子数就是基数rd,这就是"基数排序"名称的由来。
(4)算法分析
若排序文件不是以数组R形式给出,而是以单链表形式给出(此时称为链式的基数排序),则可通过修改出队和人队函数使表示箱子的链队列无须分配结点空间,而使用原链表的结点空间。人队出队操作亦无需移动记录而仅需修改指针。虽然这样一来节省了一定的时间和空间,但算法要复杂得多,且时空复杂度就其数量级而言并未得到改观。
基数排序的时间是线性的(即O(n))。
基数排序所需的辅助存储空间为O(n+rd)。
基数排序是稳定的。
由于一般排序算法涉及到元素之间的比较,如果化成比较树可以知道,这样的排序算法复杂度的下限是O(n 1.5),而基数排序没有比较元素,所以所需排序时间就少了,我们可以看到基数排序的复杂度为O(n+k),其中k(k的定义同上)为合并排列所需的时间,是个常数。
此算法适合所需排列的元素取值范围不大的情况下,否则会造成空间的消耗,比如,一共100个元素,其取值范围从1-100000,显然这个时候用计数排序是不合适的。