数据结构学习笔记——交换排序(冒泡排序和快速排序)

一、交换排序的概念

  • 交换排序通过两两比较待排序的元素,若不满足排序要求则进行交换,直到整个序列有序为止。

一、冒泡排序

(一)排序思想

按照一定的次序(从前往后或从后往前,对应递减和递增)两两比较相邻的元素,若为逆序(r[i-1]<r[i]或r[i]>r[i+1]),则进行交换,直到整个序列都比较完结束,即第一趟冒泡排序结束【第一趟冒泡排序后有一个最小或最大的元素放在排序的最终位置】。然后,继续进行下一趟冒泡排序,之前确定的最小或最大元素则不参与排序比较,第二趟后同样,序列中有一个最小或最大的元素放在排序的最终位置,依次进行下去……,直到整个序列整体有序,若某一趟冒泡排序中没有发生元素交换,则说明此时序列已整体有序,排序后递增的冒泡排序的具体代码如下:

/*冒泡排序(序列结果递增)*/
void BubbleSort1(int r[],int n) {
    
    
	for(int i=0; i<n-1; i++) {
    
    
		bool flag=false;	//flag变量用于标识本趟冒泡排序是否发生交换 
		for(int j=n-1; j>i; j--) {
    
    
			if(r[j-1]>r[j]) {
    
    	//若为逆序 
				int temp=r[j-1];	//两个元素进行交换 
				r[j-1]=r[j];
				r[j]=temp;
				flag=true;	//前一趟确定的元素不再参与下一趟冒泡排序 
			}
		}
		if(flag==false)//由于未发生交换,说明排序序列已经有序,算法提前结束 
			return;
	}
}

同样也可以写出递减的冒泡排序的代码:

/*冒泡排序(序列结果递减)*/
void BubbleSort2(int r[],int n) {
    
    
	for(int i=1; i<n; i++) {
    
    
		bool flag=false;	//flag变量用于标识本趟冒泡排序是否发生交换 
		for(int j=1; j<=n-i; j++) {
    
    
			if(r[j]>r[j-1]) {
    
    	//若为逆序 
				int temp=r[j-1];	//两个元素进行交换 
				r[j-1]=r[j];
				r[j]=temp;
				flag=true;	//前一趟确定的元素不再参与下一趟冒泡排序 
			}
		}
		if(flag==false)//由于未发生交换,说明排序序列已经有序,算法提前结束 
			return;
	}
}

例如,对于一个序列{-7,0,97,25,64,11}进行冒泡排序(升序和降序),代码如下:

#include<stdio.h>
#define MAXSIZE 100
/*创建函数*/
void Create(int r[],int n) {
    
    
	for(int i=0; i<n; i++) {
    
    
		printf("输入第%d个元素:",i+1);
		scanf("%d",&r[i]);
	}
}

/*输出函数*/
void Display(int r[],int n) {
    
    
	for(int i=0; i<n; i++)
		printf("%d ",r[i]);
}

/*冒泡排序(序列结果递增)*/
void BubbleSort1(int r[],int n) {
    
    
	for(int i=0; i<n-1; i++) {
    
    
		bool flag=false;	//flag变量用于标识本趟冒泡排序是否发生交换
		for(int j=n-1; j>i; j--) {
    
    
			if(r[j-1]>r[j]) {
    
    	//若为逆序
				int temp=r[j-1];	//两个元素进行交换
				r[j-1]=r[j];
				r[j]=temp;
				flag=true;	//前一趟确定的元素不再参与下一趟冒泡排序
			}
		}
		if(flag==false)//由于未发生交换,说明排序序列已经有序,算法提前结束
			return;
	}
}

/*冒泡排序(序列结果递减)*/
void BubbleSort2(int r[],int n) {
    
    
	for(int i=1; i<n; i++) {
    
    
		bool flag=false;	//flag变量用于标识本趟冒泡排序是否发生交换
		for(int j=1; j<=n-i; j++) {
    
    
			if(r[j]>r[j-1]) {
    
    	//若为逆序
				int temp=r[j-1];	//两个元素进行交换
				r[j-1]=r[j];
				r[j]=temp;
				flag=true;	//前一趟确定的元素不再参与下一趟冒泡排序
			}
		}
		if(flag==false)//由于未发生交换,说明排序序列已经有序,算法提前结束
			return;
	}
}

/*主函数*/
int main() {
    
    
	int n;
	int r[MAXSIZE];
	printf("请输入排序表的长度:");
	scanf("%d",&n);
	Create(r,n);
	printf("已建立的序列为:\n");
	Display(r,n);
	BubbleSort1(r,n);
	printf("\n");
	printf("递增冒泡排序后的序列为:\n");
	Display(r,n);
	BubbleSort2(r,n);
	printf("\n");
	printf("递减冒泡排序后的序列为:\n");
	Display(r,n);
}

运行结果如下:
在这里插入图片描述
冒泡排序可以用于链式存储结构,具体代码如下:

以下代码参考链接:https://blog.csdn.net/weixin_43638873/article/details/115220993

用于链式存储结构的单链表的冒泡排序(递增)代码如下:

/*冒泡排序(递增)*/
void BubbleSortList1(LinkList L) {
    
    
	LNode *s,*r=NULL;
	while(1){
    
    
		if(L->next==r)
			return;
		for(s=L->next; s->next!=r; s=s->next) {
    
    
			if(s->data>s->next->data) {
    
    		//若为逆序
				int temp=s->data;	//两个元素进行交换
				s->data=s->next->data;
				s->next->data=temp;
			}
		}
		r=s;	//使r和s的指针结点位置相同
	} 
}

用于链式存储结构的单链表的冒泡排序(递减)代码如下,只需更改若为逆序处的代码:

s->data<s->next->data

完整代码如下:

/*冒泡排序(递减)*/
void BubbleSortList2(LinkList L) {
    
    
	LNode *s,*r=NULL;
	while(1) {
    
    
		if(L->next==r)	//直至单链表整体有序 
			return;
		for(s=L->next; s->next!=r; s=s->next) {
    
    
			if(s->data<s->next->data) {
    
    		//若为逆序
				int temp=s->data;	//两个元素进行交换
				s->data=s->next->data;
				s->next->data=temp;
			}
		}
		r=s;	//使r和s的指针结点位置相同
	}
}

例如,对于一个序列{2,0,-9,24,3,11,1},采用链式存储结构的单链表,且通过冒泡排序升序和降序排序:

#include<stdio.h>
#include<stdlib.h>
typedef struct LNode {
    
    
	int data;
	struct LNode *next;
} LNode,*LinkList;

/*1、初始化一个带头结点的空单链表*/
bool H_InitList(LinkList &L) {
    
    
	L=(LNode *)malloc(sizeof(LNode));		//分配一个头结点
	if(L==NULL)		//内存不足,分配失败
		return false;
	L->next=NULL;	//头结点之后暂时还没有任何结点,表示空链表
	return true;
}

/*2、尾插法建立单链表(带头结点)*/
void H_CreateTail(LinkList L,int n) {
    
    
	LNode *last=L;
	for(int i=0; i<n; i++) {
    
    
		int number=i+1;
		printf("请输入第%d个整数:\n",number);
		LNode *s=(LNode *)malloc(sizeof(LNode)); 	//申请一个新结点s
		scanf("%d",&s->data);			//将数据读入至新结点s的数据域中
		s->next=NULL;	//将新结点s的指针域置为空,即空指针NULL
		last->next=s; 	//将新结点s插入至单链表的表尾,即last的指针域(末尾结点的后面)
		last=s;		//然后将last指针指向单链表的末尾结点,即指向新结点的后面
	}
}

/*3、单链表(带头结点)的输出*/
void H_DispList(LinkList L) {
    
    
	LNode *p;
	p=L->next;
	while(p!=NULL) {
    
    
		printf("%d ",p->data);
		p=p->next;
	}
}

/*4、冒泡排序(递增)*/
void BubbleSortList1(LinkList L) {
    
    
	LNode *s,*r=NULL;
	while(1){
    
    
		if(L->next==r)
			return;
		for(s=L->next; s->next!=r; s=s->next) {
    
    
			if(s->data>s->next->data) {
    
    		//若为逆序
				int temp=s->data;	//两个元素进行交换
				s->data=s->next->data;
				s->next->data=temp;
			}
		}
		r=s;	//使r和s的指针结点位置相同
	} 
}

/*5、冒泡排序(递减)*/
void BubbleSortList2(LinkList L) {
    
    
	LNode *s,*r=NULL;
	while(1) {
    
    
		if(L->next==r)	//直至单链表整体有序 
			return;
		for(s=L->next; s->next!=r; s=s->next) {
    
    
			if(s->data<s->next->data) {
    
    		//若为逆序
				int temp=s->data;	//两个元素进行交换
				s->data=s->next->data;
				s->next->data=temp;
			}
		}
		r=s;	//使r和s的指针结点位置相同
	}
}

/*主函数*/
int main() {
    
    
	LinkList L;		//声明一个指向单链表的指针
	int n,i;
	H_InitList(L);	//初始化一个空的单链表
	printf("请输入要建立单链表的长度:");
	scanf("%d",&n);
	H_CreateTail(L,n);
	printf("建立的单链表如下:\n");
	H_DispList(L);
	printf("\n");
	printf("冒泡排序(递增)后的单链表如下:\n");
	BubbleSortList1(L);
	H_DispList(L);
	printf("\n");
	printf("冒泡排序(递减)后的单链表如下:\n");
	BubbleSortList2(L);
	H_DispList(L);
}

运行结果如下:
在这里插入图片描述

(二)算法分析

分析
(1)冒泡排序的结束条件是一趟冒泡排序中没有发生元素交换,次数说明整个序列已经整体有序,从而结束算法;
(2)空间复杂度:由于额外辅助空间只有一个temp变量,为参数级,所以冒泡排序的空间复杂度为O(1);
(3)时间复杂度:最好情况下,即待排序结果恰好是排序后的结果,此时比较次数为n-1,移动次数和交换次数都为0,最好时间复杂度为O(n);而最坏情况下,即排好的序列刚好与初始序列相反,呈逆序排列,则此时需要进行n-1趟排序,第i趟排序中要进行n-i次比较,即比较次数=交换次数=n(n-1)/2,由于每次交换都会移动3次元素从而来交换元素,即移动次数为3n(n-1)/2,最坏时间复杂度为O(n2),而考虑平均情况下,故冒泡排序的时间复杂度为O(n2);
(4)稳定性:由于只会在某特定情况才会发生交换,所以冒泡排序是一个稳定的排序算法。
(5)适用性:冒泡排序可适用于顺序存储和链式存储的线性表,当链式存储时,可以从前向后进行比较查找插入位置。
(6)排序方式:冒泡排序是一种内部排序(In-place)。

二、快速排序

(一)排序思想

快速排序是对冒泡排序的一种改进算法,它又称为分区交换排序,通过多次划分操作来实现排序思想。每一趟排序中选取一个关键字作为枢轴,枢轴将待排序的序列分为两个部分,比枢轴小的元素移到其前,比枢轴大的元素移到其后,这是一趟快速排序,然后分别对两个部分按照枢轴划分规则继续进行排序,直至每个区域只有一个元素为止,最后达到整个序列有序。快速排序的思想是递归,其递归进行需要栈来辅助,代码如下:

/*快速排序*/
void QuickSort1(int r[],int low,int high) {
    
    
	int temp,i=low,j=high;
	temp=r[i];	//将其设为枢轴,对序列进行划分
	while(i<j) {
    
    
		while(i<j&&r[j]>=temp)	//从右往左寻找,找到小于temp的元素
			j--;
		r[i]=r[j];	//放在枢轴temp的左边
		while(i<j&&r[i]<=temp)	//从左往右寻找,找到大于temp的元素
			i++;
		r[j]=r[i];	//放在枢轴temp的右边
	}
	r[i]=temp;	//一趟快速排序结束,枢轴temp被放到其最终位置
	if(low<i-1)
		QuickSort1(r,low,i-1);	//递归,对枢轴temp的左边区域进行快速排序
	if(i+1<high)
		QuickSort1(r,i+1,high);	//递归,对枢轴temp的右边区域进行快速排序
}

例如,对一个序列{46,79,56,38,40,84}进行快速排序算法,以第一个元素为枢轴,从小到大进行快速排序的全过程。

第一趟快速排序
1、以第一个元素46为枢轴,i和j指向序列的头、尾元素,开始进行第一趟快速排序【最后,比枢轴小的元素会交换到左边,而比它的元素会交换到右边】:
在这里插入图片描述
2、整个过程保证i指针左边是比枢轴元素小的元素,j指针右边是比枢轴元素大的元素。
首先对于j,从右往左一直寻找,找到小于枢轴元素的元素,若找到则j停下(元素84大于枢轴元素,所以不用交换移动):
在这里插入图片描述
将j所指元素40放在i的位置上,j原本的位置空出,保证i指针左边是比枢轴元素小的元素,j指针右边是比枢轴元素大的元素:
在这里插入图片描述
3、然后对于i,从左往右一直寻找,找到大于枢轴元素的元素,若找到则i停下(元素40小于枢轴元素,所以不用交换移动):
在这里插入图片描述
将i所指元素79放在j的位置上,i原本的位置空出,保证i指针左边是比枢轴元素小的元素,j指针右边是比枢轴元素大的元素:
在这里插入图片描述
4、继续对于j,从右往左一直寻找,找到小于枢轴元素的元素,若找到则j停下:
在这里插入图片描述
将j所指元素38放在i的位置上,j原本的位置空出,保证i指针左边是比枢轴元素小的元素,j指针右边是比枢轴元素大的元素:
在这里插入图片描述
5、然后对于i,继续从左往右一直寻找,找到大于枢轴元素的元素,若找到则i停下:
在这里插入图片描述
将i所指元素56放在j的位置上,i原本的位置空出,保证i指针左边是比枢轴元素小的元素,j指针右边是比枢轴元素大的元素:
在这里插入图片描述
6、然后继续对于j,从右往左一直寻找,找到小于枢轴元素的元素,若找到则j停下(56大于46,所以不用交换移动),此时i和j相遇:
在这里插入图片描述
此时i=j,这个位置就是枢轴元素的最终位置,将枢轴元素46放在该位置,第一趟快速排序过程结束:
在这里插入图片描述
第一趟快速排序结束后的序列为{40,38,46,56,79,84}。
第二趟快速排序
1、继续以第一个元素为枢轴元素,此时40为枢轴,i和j指向序列的头、尾元素,开始进行第二趟快速排序:
在这里插入图片描述
2、对于j,从右往左一直寻找,找到小于枢轴元素的元素,若找到则j停下:
在这里插入图片描述
将j所指元素38放在i的位置上,j原本的位置空出:
在这里插入图片描述
3、然后对于i,从左往右一直寻找,找到大于枢轴元素的元素,若找到则i停下,此时i和j相遇:
在这里插入图片描述
4、此时i=j,这个位置就是枢轴元素的最终位置,将枢轴元素40放在该位置,第二趟快速排序过程结束:
在这里插入图片描述
由于经过两趟快速排序,整个序列已经有序,快速排序结束,最后序列结果为:
{38,40,46,56,79,84}

(二)算法分析

分析
(1)每一个序列的划分算作一趟快速排序,且每趟结束后有一个关键字到达最终位置,即枢轴元素;

这里总结一下,各种排序算法中,
每一趟排序算法的进行都能确定一个元素处于其最终位置的排序算法有以下:
①冒泡排序③简单选择排序②堆排序④快速排序
前三者能形成整体有序的子序列,而后者快速排序只确定枢轴元素的最终位置
(第n趟快速排序完成时,会有n个以上的元素处于其最终结果位置上,
即它们两边的元素分别比它大或和小)。

(2)空间复杂度:由于快速排序代码中的递归进行需要来辅助,所以其需要的空间较大。其空间复杂度与递归层数(栈的深度)有关,为O(递归层数)

若将n个要排序的元素组成一个二叉树,
这个二叉树的层数就是递归调用的层数(栈的深度),
由于在n个结点的二叉树中,其最小高度=⌊ log2n ⌋
(以2为底n的对数然后再向上取整,取比自己大的最小整数),
其最大高度=n。

所以,可知最好情况下,即最小高度,为⌊ log2n ⌋,最好空间复杂度为O(log2n);而最坏情况下,即最大高度,为n层,最坏空间复杂度为O(n);故平均情况下,为O(log2n);
(3)时间复杂度:其时间复杂度与递归层数有关,为O(n×递归层数),即取决于递归深度,若每次划分越均匀,则递归深度越低;越不均匀,则递归深度越深。在最好情况下,即每次划分比较均匀的情况,最好时间复杂度为O(nlog2n);而最坏情况下,即初始序列有序或逆序时,最坏时间复杂度为O(n2);故平均情况下,为O(nlog2n);

可知,当初始序列有序或逆序时,快速排序的性能最差,
其每次选择的都是最靠序列两边的元素,所划分的区域有一边为空,
所以待排序序列越接近无序或基本上无序,此时算法效率越高;
越接近有序或基本上有序,算法效率越低。

【由于平均情况下的所需时间与最好情况下接近,与最坏情况相比较远,所以快速排序是所有内部排序算法中平均性能最优的排序算法】
(4)稳定性:快速排序是一个不稳定的排序算法;
(6)适用性:适用于顺序存储结构,而不适用于链式存储结构;
(7)排序方式:快速排序是一种内部排序(In-place)。

三、总结

两种交换排序与前面的插入排序的总结如下表:

排序算法 空间复杂度 平均时间复杂度 最好时间复杂度 最坏时间复杂度 排序方式 稳定性 适用性
直接插入排序 O(1) O(n2) O(n) O(n2) 内部排序(In-place) 顺序存储和链式存储
折半插入排序 O(1) O(n2) O(nlog2n) O(n2) 内部排序(In-place) 顺序存储
希尔排序 O(1) 依赖于增量序列 依赖于增量序列 依赖于增量序列 内部排序(In-place) × 顺序存储
冒泡排序 O(1) O(n2) O(n) O(n2) 内部排序(In-place) 顺序存储和链式存储
快速排序 最好为O(log2n);最坏为O(n);平均情况下,为O(log2n) O(nlog2n) O(nlog2n) O(n2) 内部排序(In-place) × 顺序存储

猜你喜欢

转载自blog.csdn.net/qq_43085848/article/details/127600852