交换排序与选择排序
交换排序
两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。应用交换排序基本思想的主要排序方法有:冒泡排序和快速排序。
冒泡排序
思想
通过相邻元素之间的比较和交换,使关键字较小的元素逐渐从底部移向顶部,关键字较大的元素逐渐下移(后移),小的上浮,大的下沉,所以冒泡排序又被称为"起泡"排序。
算法实现
void BubbleSort(SeqList R, int n)
{ //采用自底向上扫描数组R[1..n]做冒泡排序
int i, j, flag;
for (i = 1; i < n; i++) //最多做n-1趟排序
{
flag = 0; // flag表示每一趟是否有交换,先置0
for (j = n; j >= i + 1; j--) //进行第i趟排序,比较n-1次,依次减1次
if (R[j].key < R[j - 1].key) {
R[0] = R[j - 1]; // R[0]作为交换时的暂存单元
R[j - 1] = R[j];
R[j] = R[0]; //相邻两个元素完成交换
flag = 1; //有交换,flag置1
}
if (flag == 0)
return;
}
}
算法分析
时间复杂度
- 最好情况
若待排序记录为有序(最好情况),则一趟扫描完成,关键比较次数为n-1次且没有移动,比较的时间复杂度为O(n)。 - 最坏情况
若初始文件是反序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。总的比较次数为:(n-1)+(n-2)+…+1= n(n-1)/2;总的移动次数为3n(n-1)/2。(每次需移动记录三次,赋值给r[0]一次,交换一次,r[0]赋给r[j]一次)在平均情况下,比较和移动记录的总次数大约为最坏情况下的一半,所以,冒泡排序算法的时间复杂度为O(n2)。
空间复杂度
冒泡排序的空间复杂度为O(1),是就地排序
稳定性
稳定的。
双向冒泡排序
思想
双向冒泡排序则是交替改变扫描方向,即一趟从下向上通过两个相邻关键字的比较,将关键字最小的记录换至最上面位置,再一趟则是从第二个记录开始向下通过两个相邻记录关键字的比较,将关键字最大的记录换至最下面的位置;然后再从倒数第二个记录开始向上两两比较至顺数第二个记录,将其中关键字较小的记录换至第二个记录位置,再从第三个记录向下至倒数第二个记录两两比较,将其中较大关键字的记录换至倒数第二个位置,依此类推,直到全部有序为止。
实现
void DbubbleSort(SeqList R, int n)
{ //自底向上、自顶向下交替进行双向扫描冒泡排序
int i = 1, j;
RecType t; // t作为排序交换记录的中间变量
int NoSwap; //表示一趟扫描是否有交换,为假无交换
NoSwap = 1; //首先假设有交换,表无序
while (NoSwap) //当有交换时做循环
{
NoSwap = 0; //置成无交换
for (j = n - i + 1; j >= i + 1; j--) //自底向上扫描
if (R[j].key < R[j - 1].key) //若反序(后面的小于前一个),即交换
{
t = R[j];
R[j] = R[j - 1];
R[j - 1] = t;
NoSwap = 1; //说明有交换
}
for (j = i + 1; j <= n - i; j++) //自顶向下扫描 i趟,j小于n-i
if (R[j].key > R[j + 1].key) //若反序(前面的大于后一个),即交换
{
t = R[j];
R[j] = R[j + 1];
R[j + 1] = t;
NoSwap = 1; //说明有交换
}
i = i + 1;
}
}
快速排序
思想
首先在当前无序区R[low…high]中任取一个记录作为排序比较的基准(不妨设为x),用此基准将当前无序区划分为两个较小的无序区R[low…i-1]和R[i+1…high],并使左边的无序区中所有记录的关键字均小于等于基准的关键字,右边的无序区中所有记录的关键字均大于等于基准的关键字,而基准记录x则位于最终排序的位置i上,这个过程称为一趟快速排序(或一次划分)。当R[low…i-1]和R[i+1…high]均非空时,分别对它们进行上述划分,直到所有的无序区中的记录均已排好序为止。
实例分析
实现
//实际是走一个完全二叉树
int Partition(SeqList R, int i, int j)
{ //对R[i]…R[j]区间内的记录进行一次划分排序
RecType x = R[i]; //用区间的第一个记录为基准
while (i < j) {
while (i < j && R[j].key >= x.key) j--; //从j所指位置起向前(左)搜索
if (i < j) {
R[i] = R[j];
i++; //后面大的数往前移动
}
while (i < j && R[i].key <= x.key) i++; //从i所指位置起向后(右)搜索
if (i < j) {
R[j] = R[i];
j--; //前面小的数往前移动
}
}
R[i] = x; //基准记录x位于最终排序的位置i上
return i;
}
void QuickSort(SeqList R, int low, int high)
{ //对顺序表R中的子区间进行快速排序
int p;
if (low < high) //长度大于1 low是整个序列较小的下标,hight是最大下标
{
p = Partition(R, low, high); //做一次划分排序
QuickSort(R, low, p - 1); //对左区间递归排序
QuickSort(R, p + 1, high); //对右区间递归排序
}
}
算法分析
时间复杂度
快速排序的时间复杂度为O(nlog2n)。快速排序方法被认为是排序时间效率非常高的一种方法,但是,当参加排序的原始序列已经是一个按值有序或基本有序的序列时,快速排序方法的时间效率将降到最低,这种情况下其时间复杂度为O(n2)
空间复杂度
快速排序的空间复杂度为O(log2n)。也就是二叉树深度
稳定性
不稳定
选择排序
基本思想:每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。 常用的选择排序方法有直接选择排序和堆排序。
直接选择排序
基本思想
每次从待排序的无序区中选择出关键字值最小的记录,将该记录与该区中的第一个记录交换位置。初始时,R[1…n]为无序区,有序区为空。第一趟排序是在无序区R[1…n]中选出最小的记录,将它与R[1]交换,R[1]为有序区;第二趟排序是在无序区R[2…n]中选出最小的记录与R[2]交换,此时R[1…2]为有序区;依此类推,做n-1趟排序后,区间R[1…n]中记录递增有序。
算法实现 2021年4月真题33就考这个算法
void SelectSort(seqList R, int n)
{
//对R作直接选择排序
int i, j, k;
for (i = 1; i < n; i++) {
//做n-1趟排序
k = i; //设k为第i趟排序中关键字最小的记录位置
for (j = i + 1; j <= n; j++) //在[i..n]选择关键字最小的记录 真题 j<=n
if (R[j].key < R[k].key)//真题这句话
k = j; //若有比R[k].key小的记录,记住该位置
if (k != i) { //与第i个记录交换 //真题填这句话,应该来说不难,通过上下文分析大概能分析出结果。
R[0] = R[i];
R[i] = R[k];
R[k] = R[0];
}
}
}
算法分析
时间复杂度
无论文件初始状态如何,在第i趟排序中选出最小关键字的记录需要做n-i次比较,总比较次数为(n-1)+(n-2)+…+1=n(n-1)/2;
当初始文件为正序时,移动次数为0;文件初态为反序时,每趟排序均要执行交换操作,每次交换需要移动3次,总的移动次数取最大值3(n-1)。
直接选择排序的平均时间复杂度为O(n2)。
空间复杂度
空间复杂度是O(1),是一个就地排序。
稳定性
不稳定
单链表操作实现
采用不带头结点的单链表作为存储结构,试编写一个直接选择排序(升序)的算法。
交换结点的数据域和关键字域值的算法。
typedef struct node { //结点类型定义
DataType data;//结点数据域
Struct node *next;//结点指针域
} ListNode;
typedef ListNode *LinkList;
void Lselectsort1(Linklist head)
{
//先找最小的和第一个结点交换,再找次小的和第二个结点交换,…,以此类推
ListNode *p, *r, *s;
ListNode q;
p = head;
while (p != NULL) { //假设链表不带头结点
s = p; //s为保存当前关键字最小结点地址指针
r = p->next;
while (r != NULL) //向后比较,找关键字值最小的结点
{
if (r->data < s->data) //若r指向结点的关键字值小,使s指向它
s = r;
r = r->next; //比较下一个
}
if (s != p) { //说明有关键字值比s的关键字值小的结点,需交换
q = (*p); //整个结点记录赋值
p->data = s->data;
s->data = q.data;
}
p = p->next; //指向下一个结点
}
}
每次选择到最小的结点后,将其脱链并加入到一个新链表中,这样可避免结点域值交换,最后将新链表的头指针返回。
typedef struct node { //结点类型定义
DataType data;//结点数据域
Struct node *next;//结点指针域
} ListNode;
typedef ListNode *LinkList;
LinkList LselectSort2(LinkList head) {
//找最小的为新表的第一个结点,找次小的作为第二个结点,…,依次类推
ListNode * p, *q, *r, *s, *t, *t1;
t = NULL //置空表,采用尾插法建立新链表
while (head != NULL) {
s = head; //先假设s指向关键字最小值的结点
p = head;
q = NULL; //q指向p的前趋结点
r = NULL; //r指向s的前趋结点
while (p != NULL)//p扫描整个链表
{
if (p->data < s-> data) { //使s指向当前关键字值小的结点
s = p;
r = q; //使r指向s的前一个结点
}
q = p;
p = p->next; //指向后继结点
}
if (s == head) //没发现更小的
head = head->next; //删除第一个结点
else
r->next = s->next; //删除最小结点
if (t == NULL)
{
t = s;
t1 = t;
} //t1指向新表的当前尾结点
else //插入新结点
{
t1->next = s;
t1 = s;
}
}
t1->next = NULL;
return t; //返回新表头指针
}
堆排序
堆定义
n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下关系:
(1) ki≤K2i且ki≤K2i+1 或(2)Ki≥K2i且ki≥K2i+1(1≤i≤n )
前者称为小根堆,后者称为大根堆。
大根堆是每一个父节点都比子节点的值要大,而小根堆则相反。
排序思想
下面以大根堆进行说明。
基本思想是先将原始序列看成是一棵完全二叉树组成,每个元素是一个节点。首先将这个二叉树的每个节点调整为大根堆,此时根节点为整棵树中值最大的节点,然后将根节点和最后一个节点进行交换,即排序出值最大的那个元素。然后继续从根节点开始进行调整,逐次排序出其他元素。
步骤
从小到大排序的步骤:(完全二叉树的顺序存储,存在数组里)
第一步:将参加排序的原始序列转换为第一个大根堆(建初始堆)
第二步:把堆的第一个元素(最大值元素)与堆的最后那个元素交换位置。
第三步:将在第二步中"去掉"最大值元素后剩下的元素组成的序列重新调整为一个新的堆。
第四步:重复第二步与第三步n-1次,直到排序结束。
过程
会分析图就可以,如下:
关键字序列为(47,33,1l,56,72,61,25,47)
-
将参加排序的原始序列转换为第一个大根堆(建初始堆)
-
将根结点(最大值)与最后一个结点调换如下图(a)所示,完成第一趟排序,第一趟排序的结果为:47 56 61 47 33 11 25 [72]
-
将第一趟排序的结果除最后一个元素外的其余结点组成的序列调整为堆,如图(b)所示,然后将根结点与倒数第2个结点调换如下图(c)所示,完成第二趟排序,第二趟排序的结果为:25 56 47 47 33 11 25 [61 72]
-
将第二趟排序的结果除最后2个元素外的其余结点组成的序列调整为堆,如图(d)所示,然后将根结点与倒数第3个结点调换如下图(e)所示,完成第三趟排序,第三趟排序的结果为:11 47 47 25 33 [56 61 72]
-
将第三趟排序的结果除最后3个元素外的其余结点组成的序列调整为堆,如图(f)所示,然后将根结点与倒数第4个结点调换如图(g)所示,完成第四趟排序,第四趟排序的结果为:11 33 47 25 [47 56 61 72]
-
将第四趟排序的结果除最后4个元素外的其余结点组成的序列调整为堆,如图(h)所示,然后将根结点与倒数第5个结点调换如图(i)所示,完成第五趟排序,第五趟排序的结果为:25 33 11 [47 47 56 61 72]
-
将第五趟排序的结果除最后5个元素外的其余结点组成的序列调整为堆,如图(j)所示,然后将根结点与倒数第6个结点调换如图(k)所示,完成第六趟排序,第六趟排序的结果为:11 25 [33 47 47 56 61 72]
-
将第六趟排序的结果除最后6个元素外的其余结点组成的序列调整为堆,如图(m)所示,然后将根结点与倒数第7个结点调换如图(n)所示,完成第七趟排序,第七趟排序的结果为:11 [25 33 47 47 56 61 72] 。排序结束(8个元素七趟排序)。
算法分析
堆排序方法是一种不稳定的排序方法,具有n个数据元素的序列,堆排序方法的排序趟数为n-1,其时间复杂度为O(nlog2n),空间复杂度为O(1)。
序思想
从小到大排序的步骤:(完全二叉树的顺序存储,存在数组里)
第一步:将参加排序的原始序列转换为第一个大根堆(建初始堆)
第二步:把堆的第一个元素(最大值元素)与堆的最后那个元素交换位置。
第三步:将在第二步中"去掉"最大值元素后剩下的元素组成的序列重新调整为一个新的堆。
第四步:重复第二步与第三步n-1次,直到排序结束。
算法分析
堆排序方法是一种不稳定的排序方法,具有n个数据元素的序列,堆排序方法的排序趟数为n-1,其时间复杂度为O(nlog2n),空间复杂度为O(1)。