[数据结构]冒泡排序快速排序

Posted 还小给个面子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[数据结构]冒泡排序快速排序相关的知识,希望对你有一定的参考价值。

文章目录

交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。冒泡排序和快速排序是两个经典的交换排序

冒泡排序

冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。

动图演示:


参考代码:

// 冒泡排序
void BubbleSort(int* a, int n)

	for (int i = 0; i < n; ++i)
	
		// 标志位,判断过程中是否已经有序
		int exchange = 0;	
		for (int j = 0; j < n - i - 1; ++j)
		
			if (a[j] > a[j + 1])
			
				Swap(&a[j], &a[j + 1]);
				exchange = 1;
			
		

		if (exchange == 0)
			break;
	

在代码中,设置了一个变量exchange来判断在第一趟冒泡排序的过程中,序列是否已经有序,如果已经有序就无需进行其余的判断操作。

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

快速排序

快速排序是对冒泡排序的一种改进,由 C.A.R.Hoare(Charles Antony Richard Hoare,东尼·霍尔)在 1962 年提出。

快速排序的基本思想是:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

void QuickSort(int* a, int left, int right)

	if (left >= right)
		return;
	
	// 按照基准值对array数组的 [left, right)区间中的元素进行划分
	int mid = PartSort(a, left, right);
	
	//[left, mid-1] mid [mid+1, right]	
	QuickSort(a, left, mid - 1);
	QuickSort(a, mid + 1, right);

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像:


将区间按照基准值划分为左右两半部分的常见方式有:hoare版本,挖坑法和前后指针法:

hoare版本

动图演示:

基本步骤:

  1. 在序列中选择一个基准值key,key一般取最左边或者最右边
  2. 定义left和right变量分别指向最左边和最右边,left从最左边开始移动,right从最右边开始移动,如果key取最左边,必须让right先移动,如果key取最右边,必须让left先移动
  3. 当right遇到比key小的值停下,left开始往右边移动,当left遇到比key大的值则停下,交换left和right指向的值,重复上述动作,直到left和right相遇,交换key和相遇位置的值

经过一次单趟排序,最终使得key左边的数据全部都小于key,key右边的数据全部都大于key。

参考代码:

int PartSort1(int* a, int left, int right)

	int keyi = left;
	while (left < right)
	
		while (left < right && a[keyi] <= a[right])
			right--;
		while (left < right && a[keyi] >= a[left])
			left++;

		if (left < right)
			Swap(&a[left], &a[right]);
	
	Swap(&a[keyi], &a[left]);
	return left;

挖坑法

动图演示:

基本步骤:

  1. 选择最左边或者最右边作为key,定义一个变量保存key值,原来key值的位置设置为pivot坑位
  2. 定义left和right分别指向最左边和最右边,如果选择最左边作为坑位,right先移动,如果选择最右边作为坑位,left先移动
  3. 选择最左边为key,right先向左边移动,当遇到比key大的值,将坑位的值设置为right指向的值,right作为新的坑位,然后left开始向右移动,当遇到比key小的值,将坑位的值设置为left指向的值,left作为新的坑位
  4. 重复上述操作,当left和right相遇,将当前坑位的值置为key,由此一来,单趟排序就结束了,最后key所在的位置,前面的值都比key要小,后面的值都比key要大

参考代码:

int PartSort2(int* a, int left, int right)

	int key = a[left];
	int pivot = left;
	while (left < right)
	
		while (left < right && a[right] > key)
			right--;

		a[pivot] = a[right];
		pivot = right;

		while (left < right && a[left] < key)
			left++;

		a[pivot] = a[left];
		pivot = left;
	
	a[pivot] = key;
	return pivot;

前后指针法

动图演示:

基本步骤:

  1. 选取最左边或者最右边作为key值,定义prev指向序列的开始,cur指向prev的后一个位置
  2. cur开始向右移动,当遇到比key小的值时,prev先向后移动一位,交换cur指向的位置和prev指向的位置的值,当遇到比key大的值时,cur向右移动,直到遇到比key小的值
  3. 当cur越界时,将prev指向的值与key交换,由此一来,就完成了一次
    单趟排序,key所在的位置,前面的值都比key要小,后面的值都比key要大

参考代码:

int PartSort3(int* a, int left, int right)

	int keyi = left;
	int prev = left;
	int cur = prev + 1;
	while (cur <= right)
	
		// ++prev != cur 保证交换的不是同一个位置
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[cur], &a[prev]);

		cur++;
	
	Swap(&a[prev], &a[keyi]);
	return prev;

快速排序的优化

三数取中

快速排序的时间复杂度是O(N*logN),这是我们假设的理想情况,每次走完一次单趟排序,以key值分割的序列的左右区间的长度是相同的,每次递归将当前序列分割成两半,一直递归下去:

但是在极端情况下,key选取到了序列中最小或者最大的数,也就是序列本身已经有序或者接近有序,那么快速排序的时间复杂度就会被降到O(N^2):

由此我们可以看出,影响快速排序效率的因素之一是单趟排序中选取的key值,key越接近于序列的中位数,快速排序的效率就越高,由此就可以得到快速排序的第一个优化三数取中:

  • 三数取中的思想是在进行单趟排序之前,先取最左边的数,最右边的数,以及中间的数的中间值做为key,这就保证了我们选取的key值一定不是序列的最大或者最小值

参考代码:

// 三数取中
int GetMidIndex(int* a, int left, int right)

	int mid = left + ((right - left) >> 1);
	if (a[mid] < a[left])
	
		if (a[left] < a[right])
		
			return left;
		
		else if (a[right] < a[mid])
		
			return mid;
		
		else
		
			return right;
		
	
	else   // a[mid] > a[left]
	
		if (a[mid] < a[right])
		
			return mid;
		
		else if (a[right] < a[left])
		
			return left;
		
		else
		
			return right;
		
	

在单趟排序的最开始,先三数取中取得key值,再与最左边的值交换作为key值(以hoare版本为例):

int PartSort1(int* a, int left, int right)

	int midi = GetMidIndex(a, left, right);
	Swap(&a[midi], &a[left]);

	int keyi = left;
	while (left < right)
	
		while (left < right && a[keyi] <= a[right])
			right--;
		while (left < right && a[keyi] >= a[left])
			left++;

		if (left < right)
			Swap(&a[left], &a[right]);
	
	Swap(&a[keyi], &a[left]);
	return left;

小区间优化

通过我们的分析,就算快速排序在理想情况下,每次都取到中间值作为key值,每次往下递归也必须开辟上一层递归层数的两倍的函数栈帧,而栈的空间是有限的,如果遇到数据比较大的序列,有可能会导致栈溢出,所以这也是快速排序可以优化的一个点。

当快速排序向下递归,越往下递归的层数会越来越多,所以我们可以控制在下层的小区间进行优化,让小区间的数不进行递归排序,进行直接插入排序,这样就无需额外开辟过多的栈空间,可以避免栈溢出。

参考代码:

// 快速排序
void QuickSort(int* a, int left, int right)

	if (left >= right)
		return;

	if (right - left + 1 < 10)
	
		InsertSort(a + left, right - left + 1);
	
	else
	
		int mid = PartSort1(a, left, right);
		//[left, mid-1] mid [mid+1, right]
		QuickSort(a, left, mid - 1);
		QuickSort(a, mid + 1, right);
	


快速排序的非递归实现

为了解决快速排序在遇到海量数据的情况下,我们可以实现一个非递归的版本来解决这个问题。

参考代码:

// 快速排序非递归实现
void QuickSortNonR(int* a, int left, int right)

	Stack st;
	StackInit(&st);
	StackPush(&st, left);
	StackPush(&st, right);

	while (!StackEmpty(&st))
	
		int end = StackTop(&st);
		StackPop(&st);
		int begin = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort1(a, begin, end);
		//[begin, keyi - 1] keyi [key + 1, end] 
		if (begin <= keyi - 1) 
			StackPush(&st, begin);
			StackPush(&st, keyi - 1);
		

		if (keyi + 1 <= end) 
			StackPush(&st, keyi + 1);
			StackPush(&st, end);
		
	
	StackDestroy(&st);


快速排序的特性总结:

  1. 快速排序整体的综合性能和使用场景都是比较好的
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

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

目录

一、交换排序的概念

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

一、冒泡排序

(一)排序思想

按照一定的次序(从前往后或从后往前,对应递减和递增)两两比较相邻的元素,若为逆序(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的右边区域进行快速排序

(二)算法分析

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

这里总结一下,各种排序算法中,
每一趟排序算法的进行都能确定一个元素处于其最终位置的排序算法有以下:
①冒泡排序③简单选择排序②堆排序④快速排序
前三者能形成整体有序的子序列,而后者快速排序只确定枢轴元素的最终位置。

(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(nlog2n)O(nlog2n)O(n2)内部排序(In-place)×顺序存储

以上是关于[数据结构]冒泡排序快速排序的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法 4:排序算法,选择/插入/冒泡/希尔/快速/归并

原创系列 |「冒泡排序」提升为「快速排序」,都发生了什么?

Go语言实现冒泡排序选择排序快速排序及插入排序的方法

php四种基础算法:冒泡,选择,插入和快速排序法

重温7种排序算法 第一篇,交换排序算法:冒泡排序快速排序

php四种基础算法:冒泡,选择,插入和快速排序法