9.4 选择排序
选择排序的基本思想是:第一趟在有n个数据元素的数据表中选出关键字最小的数据元素,然后在剩下n-1个数据元素中再选取关键字最小(整个数据表中次小)的数据元素,如此重复,每一趟(如第i趟,i=1,2,…,n-1)总是在当前剩下的n-i+1个待排序数据元素中选出关键字最小的数据元素,作为有数据元素序列的第i个数据元素。等到第n-1趟选择结束,待排序数据元素仅剩下一个时就不用再选了,按选出的先后次序所得到的数据元素序列即为有序序列,排即告完成。
9.4.1 简单选择排序
简单选择排序(simple selection sort) 是一种简单的排序方法,它可以在顺序表存储结构下实现,也可以在链表存储结构下实现。
1.顺序表上的简单排序
- 在顺序表存储结构下(设n个数据元素存储在向量elem[0]~elem[n-1]中),简单选择排序的算法基本思想如下。
1)开始时设i的初始值为0。
2)如果i<n-1,在数据元素序列elem[i]~elem[n-1]中,选出具有最小关键字的数据元素 elem[k];否则算法结束。
3)若elem[k]不是这组数据元素中的第一个数据元素(即i≠K),则将elem[k]与elem[i]这两个数据元素进行交换位置。
4)令i=i+1,转步骤2)。
2. 代码实现
template<class ElemType> void SimpleSelectionSort(ElemType elem[],int n)
{
int k;
for(int i=0;i<n-1;i++)//第i趟简单选择排序
{
k=i;//k记录elem[i..n-1]中最小下标
for(int j=i+1;j<n;j++)
if(elem[j]<elem[k])
k=j;
if(k!=i)
Swap(elem[i],elem[k]);
}
}
- 在简单选择排序中,关键字比较次数与数据元素的初始排列无关。假定整个数据表有n个数据元素,总共需要n-1趟的选择,第i(i=0,1,…, n-2)越选择具有最小关键字数据元素所需的比较次数总是n-i-1 次。因此,整个排序过程中总的关键字比较次数为:(n-1)+(n-2)+…+1=n(n-1)/2。
- 数据元素的移动次数与数据表中数据元素的初始排列有关。
(1)当这组数据元素的初始状态是按其关键字从小到大有序时,每一趟选择后都不需要进行交换,数据元素的移动次数为0,达到最小值;
(2)最坏情况是每一趟选择后都要进行元素交换,一趟交换需要移动数据元素三次。所以,总的数据元素移动次数为3(n-1)。
可见简单选择排序总的时间复杂度为O(n2)。由于在简单选择排序过程中,交换数据元素一般不是在相邻位置的数据元索之问进行,因此它不是一种稳定的排序方法。
2.链表上的简单选择排序
- 在链表存储结构下简单选择排序(link selection sort) 的算法基本和在顺序存储结构下的算法类似,仅仅当在待排序数据元素序列中选出具有最小关键字的数据元素后一个要进行链表的插入,一个是进行位置对调。
1)开始时设h 指向数据表的第一个数据元素结点,设置搜索指针p指向h,elem[0].next 设为-1,表示有序链表为空表。
2)如果p!=-1,表示数据表中还有数据元素没有被选出,继续步骤3);否则数据表中所有数据元素都已经被选出,并插入有序链表中,所以算法结束。
3)在数据表中选出具有最大关键字的数据元素结点,并由q指向,pq指向q的前驱结点(如果存在)。
4)将q所指结点插入到有序链表的第一个数据元素结点之前。
5)令p=h转步骤 2),准备下一次选择。
- 代码实现
template<class ElemType> struct Node
{
ElemType data;
int next;
};
template<class ElemType> void SimpleSelectionSort(Node<ElemType> elem[])
{
int p,q,pp,pq,h;
h=p=elem[0].next;
elem[0].next=-1;//形成初始有序链表
while(p!=-1)
{
q=pq=pp=p;//设置初始搜索指针
p=elem[p].next;
while(p!=-1)//选出当前具有最大关键字的数据元素,并由q指向
{
if(elem[p].data>=elem[q].data)
{
q=p;
pq=pp;
}
pp=p;
p=elem[p].next;
}
if(h==q)//将最大关键字的数据元素插入有序链表的表首
{
h=elem[q].next;
elem[q].next=elem[0].next;
elem[0].next=q;
}
else
{
elem[pq].next=elem[q].next;
elem[q].next=elem[0].next;
elem[0].next=q;
}
p=h;//准备下一次选择
}
}
- 与在顺序表上的直接插入排序一样。在链表上进行简单选择排序,关键字比较次数与数据元素的初始排列无关,总的关键字比较次数为:(n-1)+(n-2)+…+1=n(n-1)/2。
- 在链表上进行简单选择排序不需要进行数据元素的移动,每一趟选出数据元素后只需将其插入有序表的第一个数据元素之前。可见在链表上进行简单选择排序总的时间复杂度也为O(n2)。由于在链表上的简单选择排序过程中,不进行数据元素位置的交换,且对于关键字相同的数据元素,原来在后面的元素先选出插入链表,使得排序前后两个关键字相同的数据元素的相对位置不变,因此它是一种稳定的排序方法。
小结
简单选择排序 | 顺序表 | 链表 |
---|---|---|
比较次数 | n个元素,n-1次选择,第i趟比较n-i-1次,总共n(n-1)/2 | n(n-1)/2 |
最好情况 | 初始从小到大有序,不需交换,移动0次 | 不许交换 |
最坏情况 | 每一趟选择后都要进行元素交换,一趟交换需要移动数据元素3次 | – |
时间复杂度T(n) | O(n2) | O(n2) |
空间复杂度S(n) | O(1) | 不需额外空间 |
稳定性 | 不稳定 | 稳定 |
9.4.2 锦标赛排序
- 简单选择排序要执行n-1趟选择,总的关键字比较次数为n(n-1)/2。当n较大时,其比较次数相当大。这是因为它没有把前一趟比较的结果保留下来,在后一趟选择时,把前一趟已做过的比较又重复做了一遍。锦标赛排序克服了这一缺点,它的思想与体育比赛类似。
- 首先将n个数据元素两两分组,分别按关键字进行比较,得到n/2个比较的优胜者(关键字小者),作为第一步比较的结果保留下来,然后对这n/2个数据元素再两两分组,分别按关键字进行比较。如此重复,直到选出一个关键字最小的数据元素为止。
- 相当于一棵完全二叉树的叶结点,它存放的是所有参加排序的数据元素。叶结点上面一层的非叶结点是按叶结点关键字进行两两比较的结果。最顶层是树的根,表示最后选择出来的具有最小关键字的数据元素。由于每次两两比较的结果总是把关键字小者作为优胜者上升到双亲结点,所以称这种比赛树为优胜者树。位于最底层的叶结点叫作优胜者树的外结点,非叶结点称为优胜者树的内结点。
- 锦标赛排序构成的树是完全二叉树,其高度为⌈log2n⌉+1,其中n为数据表中数据元素个数。因此,除第一次选出具有最小关键字的数据元素需要进行n-1次关键字比较外,重构优胜者树,选出具有次小、再次小……关键字的数据元素所需的关键字比较次数均不超过 O(log2n)。所以,总的关键字比较次数为O(nlogn)。数据元素的移动次数与关键字的比较次数相当,所以锦标赛排序总的时间复杂度为O(nlog2n)。
- 这种排序方法虽然减少了许多排序时间,但是使用了较多的附加存储。如果有n个数据元素,必须使用至少 2n-1个结点来存放优胜者树。如果约定当关键字相同的两个数据元素比较时,相对位置在前面的数据元素“胜出”,则可维持数据表的稳定性。
- 锦标赛排序代码参考:https://www.cnblogs.com/james1207/p/3323115.html
#include <iostream>
#include <cassert>
using namespace std;
#define MAX 0x7fffffff
struct node{
int nData;
int id;
node(int n,int i){nData=n;id=i;}
};
node* BuildTree(int data[],int len,int &nTreeSize)
{
int nNodes = 1;
while(nNodes<len)//为了构建完全二叉树,不够的要补
nNodes <<= 1;
nTreeSize = nNodes*2 - 1;
node *trees = (node*)malloc(sizeof(node)*nTreeSize);
assert(trees);
for(int i=nNodes-1; i<nTreeSize; i++){
int idx = i - (nNodes - 1);
if(idx<len)
trees[i] = node(data[idx],i);
else
trees[i] = node(MAX,-1);//对于补充的数据,我们初始化成最大。
}
for(int i=nNodes-2; i>=0; --i){ //初始化,前面白色节点,指向孩子节点的最小值
if(trees[i*2+1].nData < trees[i* 2+2].nData)
trees[i] = trees[i*2+1];
else
trees[i] = trees[i*2+2];
}
return trees;
}
void Adjust(node *data, int idx)//当去除最小元素以后,我们要调整数组
{
while(idx != 0) //从后向前调整
{
if(idx%2 == 1)//当前id是奇数,说明并列的是idx + 1, 父节点是 (idx-1)/2
{
if(data[idx].nData < data[idx + 1].nData) //idx+1为兄弟节点
data[(idx-1)/2] = data[idx];
else
data[(idx-1)/2] = data[idx+1];
idx = (idx-1)/2;
}
else
{
if(data[idx-1].nData < data[idx].nData) //idx-1为兄弟节点
data[idx/2-1] = data[idx-1];
else
data[idx/2-1] = data[idx];
idx = (idx/2-1);
}
}
}
void sort(node *trees,int len)//返回排序的结果
{
int dataLen = len/2+1;
int *data = new int[dataLen];
assert(data);
for(int i=0; i<dataLen; i++){
data[i] = trees[0].nData;//输出
trees[trees[0].id].nData = MAX;//输出节点替换为最大值
Adjust(trees,trees[0].id);//调整树
}
for(int i=0;i<dataLen;i++){
cout<<data[i]<<" ";
}
cout<<endl;
delete[] data;
}
void PrintArr(node *arr,int len)
{
assert(arr && len>0);
for(int i=0; i<len; ++i){
cout<<arr[i].nData<<" ";
}
cout<<endl;
}
int main()
{
int treeLen;
node *trees;
int arr[] = {3,4,1,6,2,8,7,9};
trees = BuildTree(arr,8,treeLen);
PrintArr(trees,treeLen);
sort(trees,treeLen);
delete[] trees;
system("pause");
return 0;
}
9.4.3 堆排序
- 堆排序的算法基本思想如下。
1)对数据表中的数据元素,利用堆的调整算法形成初始堆。
2)输出堆顶元素。
3)对剩余元素重新调整形成堆。
4)重复执行第2)、3)步,直到所有数据元素被输出。 - 代码实现
template<class ElemType> void FilterDown(ElemType elem[],int low,int high)
{
int f=low,i=2*low+1;
ElemType e=elem[low];
while(i<=high)//f为被调整节点,i为f的最大孩子
{
if(i<high && elem[i]<elem[i+1])//i取f左右孩子中值大的孩子
i++;
if(e<elem[i])//孩子的值大于其双亲,则向下调整
{
elem[f]=elem[i];
f=i;
i=2*f+1;
}
else
break;
}
elem[f]=e;
}
- 如果建立的堆满足最大堆的条件,则堆的第一个数据元素 elem[0]具有最大的关键字,将elem[0]与 elem[n-1]对调,把具有最大关键字的数据元素交换到最后,再对数据表前面的n-1 个数据元素使用堆的调整算法,重新建立最大堆。结果把具有次最大关键字的数据元素又上浮到堆顶(即 elem[0]位置),再交换elem[0]和 elem[n-2],……如此反复执行n-1次,最后得到全部排序好的数据元素序列。
template<class ElemType> void HeapSort(ElemType elem[],int n)
{
int i;
for(i=(n-2)/2; i>=0; i--) //初始建堆,将elem[0..n-1]调整成最大堆
FilterDown(elem,i,n-1);
for(i=n-1; i>0; i--)
{
Swap(elem[0],elem[i]);//将堆顶元素和当前未经排序的子序列elem[0..i]中最后一个元素交换
FilterDown(elem,0,i-1);//将elem[0..i-1]调整为最大堆
}
}
- 堆排序算法的时间复杂性可用关键字的比较次数来测度。
- 若设堆中有n个结点,且2k-1<=n<2k,则对应的完全二叉树的高度为k。在第i层上的结点数不超过2i-1(i=1,2,…,k)。
- 在第一个形成初始堆的for循环中对每一个非叶结点调用了一次堆调整算法FilterDown(),一个数据元素每下调一层需要进行两次关键字的比较,最多下调到最底层。
因此,该循环所用的计算时间为:
其中,i是层序号,2i-1是第i层的结点最多数目,k-i-1)是第i层结点下调到最底层(第k层)所需的调整次数。 - 在第二个for循环中,调用了n-1次FilterDown()算法,因为每次调用总是将位于根上的数据元素最多下调到当前堆的最底层,所以该循环的计算时间为O(nlog2n)。
因此,堆排序的时间复杂性为O(nlog2n)。
- 该算法的附加存储是用于建堆的工作变量和在第二个for循环中执行数据元素交换时需用的一个工作变量。因此,该算法的空间杂性为O(1)。
- 显然,堆排序是一种不稳定的排序方法。
总结
- 简单选择排序
- 可以顺序,也可以链式存储
- 每次将未排序中最小元素放在第一个
- 总时间复杂度O(n2)。
- 顺序存储结构不稳定,链式存储结构稳定。
- 锦标赛排序
- 优胜者树(相当于完全二叉树),高度h=⌈log2n⌉+1;
- 总的关键字比较次数为O(nlogn);移动次数与关键字的比较次数相当;总的时间复杂度为O(nlog2n)。
- 【优点】减少许多排序时间
- 【缺点】使用了较多的附加存储(如果有n个数据元素,必须使用至少 2n-1个结点来存放优胜者树)
- 具有稳定性
- 堆排序
- 用最大堆的调整,将堆顶元素放到堆的最后一位。
- 堆中有n个结点,且2k-1<=n<2k,则对应的完全二叉树的高度为k。在第i层上的结点数不超过2i-1。
- 第一个形成初始堆的for循环中,调整每一个非叶结点,一个数据元素每下调一层需要进行两次关键字的比较。循环所用时间<4n。
- 在第二个for循环中,调用了n-1次FilterDown()算法,将根的数据元素最多下调到当前堆的最底层,计算时间为O(nlog2n)。
- 因此,堆排序的时间复杂性为O(nlog2n)。
- 空间杂性为O(1),建堆和交换暂存变量。
- 不稳定