【排序三】交换排序(冒泡排序&&快速排序)

【排序一】插入排序(直接插入排序&&希尔排序)

【排序二】选择排序(选择排序&&堆排序)

一、冒泡排序

1、基本思想

    冒泡排序(Bubble Sort,台湾译为:泡沫排序或气泡排序)是一种简单的排序算法。为什么会叫做冒泡排序呢?这是由于它的算法思想就类似于鱼儿在河里吐泡泡的场景,例如升序排列一列数,它会两两相邻的数据进行比较,如果前者大于后者就交换,重复此番工作直到交换到最后两个数据,第一趟冒泡排序已经完成,最大的数据被冒到数组的最后一个位置,继而缩小冒泡的区间,又从头开始第二趟冒泡,直到次大数被放在倒数第二个位置,以此类推,直到所有数据被冒到合适位置,冒泡排序就算完成。


图解(例如升序):



以上图片则是第一趟排序后的结果,由图可知,经过一趟冒泡后,最大的元素出现在了它最终该出现的位置。


以此类推,最后一趟排序过程为:



比较以上三幅图,我们可知道含六个元素的数组第一趟冒泡需要5次比较,第二趟需要4次交换比较,以此类推,我们可以总结出含n个元素的一组数总共需要n-1(i)趟排序,每趟排序需要比较n-1-i(j)次。


2、算法实现步骤

1>比较相邻的元素。如果第一个比第二个大,就交换他们两个。

2>对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

3>针对所有的元素重复以上的步骤,除了最后一个。

4>持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。


3、排序效果图



4、时间复杂度&&空间复杂度

从以上分析中可得出,冒泡排序需要n-1(i)趟冒泡,每趟冒泡需要比较n-1-i(j)次比较,总共的比较次数为(n-1)+(n-2)+……+1,所以冒泡排序算法最坏情况和平均复杂度是O(n²)。

由于占用空间有限,冒泡排序的空间复杂度为O(1)


5、代码实现:

#pragma  once
#include <iostream>
#include <assert.h>
using namespace std;

void BubbleSort(int* a,size_t n)
{
	for (size_t i = 0; i < n; ++i)
	{
		for (size_t j = 0; j < n-1-i; ++j)
		{
			if (a[j] > a[j+1])
			{
				swap(a[j],a[j+1]);
			}
		}
	}
}

//冒泡排序优化
void BubbleSort1(int* a,size_t n)
{
	size_t flag = 0;
	for (size_t i = 0; i < n; ++i)
	{
		flag = 0;
		for (size_t j = 0; j < n-1-i; ++j)
		{
			if (a[j] > a[j+1])
			{
				swap(a[j],a[j+1]);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

void PrintArray(const int* a,const size_t n)
{
	for (size_t i = 0; i < n; ++i)
	{
		cout<<a[i]<<" ";
	}
	cout<<endl;
}

void TestBubbleSort()
{
	int a[] = {2,5,4,9,3,6,8,7,1,0};
	size_t sz = sizeof(a)/sizeof(a[0]);

	BubbleSort(a,sz);
	//BubbleSort1(a,sz);
	PrintArray(a,sz);
}


运行结果:



注意:冒泡排序的各种优化详见:冒泡排序


二、快速排序

1、基本思想

       说到底,快速排序就是冒泡排序的一种改进,冒泡排序是通过每一趟冒泡将最大值(最小值)放到恰当位置,而快速排序則是每趟排序从待排序区间选一个基准值(也称作枢纽值),将比它小的数据全放在其左边,将比它大的值放在其右边然后递归其左右子区间对其排序,一层层递归下去,某区直到间只剩一个数据时,停止递归,此子区间已经算是有序,继而向其上层区间返回,一层层向上返回,当首次枢纽值的左右区间均已有序时,整个排序就算完成。当然,No pic you say a JB,不懂的请看下面图解:

图解(假设升序):



2、算法的实现步骤

1>从数列中挑出一个元素,称为 “基准”(pivot),

2>重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分       区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

3>递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。


3、排序效果图



4、算法实现及优化

吐舌头算法目前,我了解到的快速排序的实现算法有三种,分别是左右指针法、挖坑法、前后指针法

(一)左右指针法

【算法执行步骤】

1>用两个指针left和right用来标识区间范围,由于我将key值定义为区间最右边的值,所以要左指针开始走

2>左指针向右找比key值大的数据,找到后停下来,现在右指针向左开始找比key小的数据,找到后将左右指针的值交换。

3>左指针继续找比key大的值,右指针继续找比key小的值,找到后交换,直到左右指针相遇,然后将左指针所在位置的值赋值为key。

4>此时比key值小的数据全部在key的左边,比key大的值全在key的右边。

5>按照上述同样的方法递归以上key值的左右区间,使之有序后排序完成。


算法思想如图



【实现代码】

//左右指针法
int PartSort1(int* a,int left,int right)
{
	int key = right;//利用key作为基准值的下标

	while (left < right)
	{
		//左指针向右找第一个比key大的数
		while (left < right && a[left] <= a[key])
		{
			++left;
		}
		//右指针向左扎找第一个比key的数
		while (left < right && a[right] >= a[key])
		{
			--right;
		}
		//交换左右指针所指的值
		if (a[left] != a[right])
		{
			std::swap(a[left],a[right]);
		}
	}
	//将key值放到正确位置上
	swap(a[left],a[key]);

	return left;
}



(二) 挖坑法

【算法指行步骤】

1>用两个指针left和right用来标识区间范围,初始坑设置到key值的地方。由于我将key值定义为区间最右边的值,所以要左指针开始走

2>左指针向右找比key值大的数据,找到后将左指针所指的数据填入坑中,将坑更新为左指针所指位置。现在右指针向左开始找比key小的数据,找到后将右指针所指的数据填入坑中,将坑更新为右指针所指位置

3>类似步骤2>左右指针继续走,向中间靠拢直到左右指针相遇,然后将坑处的值值赋值为key。

4>此时比key值小的数据全部在key的左边,比key大的值全在key的右边。

5>按照上述同样的方法递归以上key值的左右区间,使之有序后排序完成。


【算法思绪图】



【代码实现】

//挖坑法
int PartSort2(int*a,int left,int right)
{
	int key = a[right];//将区间最右侧数据基准值
	int blank = right;//首次将坑设置到key处

	while (left < right)
	{
		//左指针向右找比key大的数据
		while (left < right && a[left] <= key)
		{
			++left;
		}
		a[blank] = a[left];//用找到的数据填坑
		blank = left;//更新坑所在位置

		//右指针想左找比key小的数据
		while (left < right && a[right] >= key)
		{
			--right;
		}
		a[blank] = a[right];//用找到的数据填坑
		blank = right;//更新坑
	}
	a[blank] = key;//最后用key值填坑
	return blank;
}


(三)前后指针法

【算法的执行步骤】

1>给两个指针cur和prev,cur初始值指向区间最左端,prev指向cur的前一个位置;

2>让cur向后走找比key小的值,找到之后++prev,如果prev!=cur,交换二者所指数据,如果找不到找比key小的值,cur一直向后走,直到走到区间右边界停止;

3>当cur走到右边界时,++prev,交换二者所指数据;

4>缩小边界,递归排序子区间,直到子区间只有一个数返回,排序就算完成。


【算法的思绪图】



【代码实现】

//3前后指针法
int PartSort3(int* a,int left,int right)
{
	int key = right;//key保存基准值的下标
	int cur = left;
	int prev = cur - 1;

	while (cur != right)
	{
		if (a[cur] < a[key] && a[++prev] != a[cur])
		{
			swap(a[cur],a[prev]);
		}
		++cur;
	}
	swap(a[++prev],a[cur]);
	return prev;
}


吐舌头快排的性能分析(时间复杂度&&空间复杂度

      快速排序是一种快速的分而治之的算法,它是已知的最快的排序算法,其平均运行时间为O(N*1ogN) 。它的速度主要归功于一个非长紧凑的并且高度优化的内部循环。但是他也是一种不稳定的排序,当基准数选择的不合理的时候他的效率又会编程O(N*N)。

快速排序的最好情况:
  快速排序的最好情况是每次都划分后左右子序列的大小都相等,其运行的时间就为O(N*1ogN)。

快速排序的最坏情况:
  快速排序的最坏的情况就是当分组重复生成一个空序列的时候,这时候其运行时间就变为O(N*N)

快速排序的平均情况:
  平均情况下是O(N*logN),证明省略。

综上所述,快速排序的时间复杂度为O(N*lgN),快速排序的空间复杂度为O(lgN).

吐舌头 快速排序的优化

优化1:三数取中
  因为虽然快速排序整体的效率可观,但是当最坏情况发生时它的效率就会降低,为了降低最坏情况发生的概率,我们可以做如下改进。
  当我们每次划分的时候选择的基准数接近于整组数据的最大值或者最小值时,快速排序就会发生最坏的情况,但是每次选择的基准数都接近于最大数或者最小数的概率随着排序元素的增多就会越来越小,我们完全可以忽略这种情况。但是在数组有序的情况下,它也会发生最坏的情况,为了避免这种情况,我们在选择基准数的时候可以采用三数取中法来选择基准数。
三数取中法:
  选择这组数据的第一个元素、中间的元素、最后一个元素,这三个元素里面值居中的元素作为基准数。

【代码实现】
//快排优化1:三数取中法
int GetMidIndex(int* a,int left,int right)
{
	int mid = left + ((right - left) >> 1);

	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else//a[left]>=a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}


优化2:小区间优化
  当划分的子序列很小的时候(一般认为小于13个元素左右时),我们再使用快速排序对这些小序列排序反而不如直接插入排序高效。因为快速排序对数组进行划分最后就像一颗二叉树一样,当序列小于13个元素时我们再使用快排的话就相当于增加了二叉树的最后几层的结点数目,增加了递归的次数。所以我们在当子序列小于13个元素的时候就改用 直接插入排序来对这些子序列进行排序。

【代码实现】
void InsertSort(int* a,size_t n)//升序
{
	int end = 0;
	for (size_t i = 1; i < n; ++i)
	{
		int tmp = a[i];
		end = i - 1;

		while(end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end+1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end+1] = tmp;
	}
}


优化3:非递归实现快排
    从优化2中我们可以看出只有当数据量庞大时 快速排序才会显现它的快速高效,可是反过来想,当数据较多时,我们递归的子区间肯定也随之增多,但是每次递归都会创建栈帧,大大增大了函数调用的开销,影响函数执行效率,基于这个原因,我们就必须实现快速排序的非递归实现来减少函数调用开销,以提高快排效率。

【代码实现】
#include <stack>
//快排优化3:非递归实现
void QuickSortNoneR(int* a,int left,int right)
{
	assert(a);
	stack<int> s;//创建一个栈
	s.push(right);
	s.push(left);

	while(!s.empty())
	{
		int start = s.top();//先取左边界
		s.pop();
		int end = s.top();//再取右边界
		s.pop();
		//int div = PartSort1(a,start,end);
		//int div = PartSort2(a,start,end);
		int div = PartSort3(a,start,end);

		if (start < div-1)
		{
			s.push(div - 1);
			s.push(start);
		}
		if (end > div+1)
		{
			s.push(end);
			s.push(div + 1);
		}
	}
}


吐舌头快速排序的完整代码
#pragma  once
#include <iostream>
#include <assert.h>
using namespace std;

void PrintArray(const int* a,const size_t n)
{
	for (size_t i = 0; i < n; ++i)
	{
		cout<<a[i]<<" ";
	}
	cout<<endl;
}

//快排优化1:三数取中法
int GetMidIndex(int* a,int left,int right)
{
	int mid = left + ((right - left) >> 1);

	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else//a[left]>=a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

//左右指针法
int PartSort1(int* a,int left,int right)
{
	int mid = GetMidIndex(a,left,right);
	swap(a[mid],a[right]);

	int key = right;//利用key作为基准值的下标

	while (left < right)
	{
		//左指针向右找第一个比key大的数
		while (left < right && a[left] <= a[key])
		{
			++left;
		}
		//右指针向左扎找第一个比key的数
		while (left < right && a[right] >= a[key])
		{
			--right;
		}
		//交换左右指针所指的值
		if (a[left] != a[right])
		{
			std::swap(a[left],a[right]);
		}
	}
	//将key值放到正确位置上
	swap(a[left],a[key]);

	return left;
}


//挖坑法
int PartSort2(int*a,int left,int right)
{
	int mid = GetMidIndex(a,left,right);
	swap(a[mid],a[right]);

	int key = a[right];//将区间最右侧数据基准值
	int blank = right;//首次将坑设置到key处

	while (left < right)
	{
		//左指针向右找比key大的数据
		while (left < right && a[left] <= key)
		{
			++left;
		}
		a[blank] = a[left];//用找到的数据填坑
		blank = left;//更新坑所在位置

		//右指针想左找比key小的数据
		while (left < right && a[right] >= key)
		{
			--right;
		}
		a[blank] = a[right];//用找到的数据填坑
		blank = right;//更新坑
	}
	a[blank] = key;//最后用key值填坑
	return blank;
}

//3,前后指针法
int PartSort3(int* a,int left,int right)
{
	int mid = GetMidIndex(a,left,right);
	swap(a[mid],a[right]);

	int key = right;//key保存基准值的下标
	int cur = left;
	int prev = cur - 1;

	while (cur != right)
	{
		if (a[cur] < a[key] && a[++prev] != a[cur])
		{
			swap(a[cur],a[prev]);
		}
		++cur;
	}
	swap(a[++prev],a[cur]);
	return prev;
}
//快排优化2:小区间优化
void InsertSort(int* a,size_t n)//升序
{
	int end = 0;
	for (size_t i = 1; i < n; ++i)
	{
		int tmp = a[i];
		end = i - 1;

		while(end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end+1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end+1] = tmp;
	}
}
void QuickSort(int* a,int left,int right)
{
	assert(a);
	if (left < right)
	{
		int kiv = PartSort1(a,left,right);
		int kiv = PartSort2(a,left,right);
		int kiv = PartSort3(a,left,right);

		//快排优化2:小区间优化
		if (right - left < 2)//数据较少直接用插入排序更优
		                     (13可能更合适,写2为了方便测试)
		{
			InsertSort((a + left),(right - left +1));
		}
		else//数据量大则选用快排
		{
			QuickSort(a,left,kiv-1);
			QuickSort(a,kiv+1,right);
		}
	}
}

#include <stack>
//快排优化3:非递归实现
void QuickSortNoneR(int* a,int left,int right)
{
	assert(a);
	stack<int> s;//创建一个栈
	s.push(right);
	s.push(left);

	while(!s.empty())
	{
		int start = s.top();//先取左边界
		s.pop();
		int end = s.top();//再取右边界
		s.pop();
		//int div = PartSort1(a,start,end);
		//int div = PartSort2(a,start,end);
		int div = PartSort3(a,start,end);

		if (start < div-1)
		{
			s.push(div - 1);
			s.push(start);
		}
		if (end > div+1)
		{
			s.push(end);
			s.push(div + 1);
		}
	}
}


void TestQuickSort()
{
	int a[] = {2,0,4,9,3,6,8,7,1,5};

	size_t sz = sizeof(a)/sizeof(a[0]);

	QuickSort(a,0,sz-1);
	//QuickSortNoneR(a,0,sz-1);

	PrintArray(a,sz);
}


评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值