八大排序算法

Posted Jqivin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了八大排序算法相关的知识,希望对你有一定的参考价值。


前言

本文一共介绍常见的八种排序算法,案例仅供参考。

冒泡排序

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

1.算法描述

首先,从第一个元素开始,两两进行比较,把大的移到后面,经过一趟排序之后,最大的数据就到了数据的最后端。一共要进行n-1趟(假设是n个数据)

2.图示
黄色的已经有序,不需要再进行比较。

3. 代码实现

//原始解法
void BubbleSort(vector<int>& arr)
{
	for (int i = 0; i < arr.size()-1; i++)             //遍历的次数
	{ 
		for (int j = 0; j < arr.size() - 1 - i; j++)         //从头再次开始遍历,寻找最大的元素放到后面
		{
			if (arr[j] > arr[j + 1])
			{
				swap(arr[j], arr[j + 1]);
			}
		}
	}
}

优化之后

void BubbleSort(vector<int>& arr)
{
	for (int i = 0; i < arr.size()-1; i++)             //遍历的次数
	{ 
		bool stop = true;
		for (int j = 0; j < arr.size() - 1 - i; j++)         //从头再次开始遍历,寻找最大的元素放到后面
		{
			if (arr[j] > arr[j + 1])
			{
				swap(arr[j], arr[j + 1]);
				stop = false;
			}
		}
		if (stop)        //如果stop为空,证明没有产生交换的过程,数据已经完全有序,直接返回
		{
			return;
		}
	}
}

快速排序

1.算法描述

1.首先找到一个基准元素(数组的第一个),通过一次排序使它找到找到自己的位置(左边的都比这个元素的值小,右边的元素都比这个值大)。
2.按照同样的思想分别处理前一段数据和后一段数据。(对左右两部分分别执行上面的操作);

2.图示

3.代码实现

   分为递归实现和非递归实现两种方法

  • 递归实现

按照一个基准数据,将待排序列分成两部分——OneQuick()函数

  1. 记录开头和结尾,分别记为i,j。用tmp保存基准元素的值。在i < j的时候,执行下面的操作
  2. 通过j从后往前寻找第一个比基准元素小的元素,找到之后把它放到i的位置
  3. 通过i从前向后寻找第一个比基准元素大的元素,找到之后把它放到j的位置
  4. i == j的之后循环退出,这时候把tmp的值放到i或者j的位置,这个操作就完成了。

使用递归的方法处理前一段和后一段的数据 ——Quick()函数

  1. 判断是否处理完的依据就是看这个段中还有几个元素,当元素多于一个的时候,就认为没有完成。

做一层简单的封装,作为接口——QuickSort()函数

int OneQuick(vector<int>& arr,int start,int end)   //start和end分别为传进来数据的开始和结尾
{
	int i = start, j = end;
	int tmp = arr[start];
	while (i < j)
	{
		while (i < j && arr[j] >= tmp) 
			j--;		 //当i < j 的时候并且 arr[j]大于 tmp的时候,说明j所只想的元素的位置不对,应该移动
		arr[i] = arr[j];

		while (i < j && arr[i] <= tmp) 
			i++;
		arr[j] = arr[i];
	}
	arr[i] = tmp;            //最后一步不要忘了
	return i;
}

void Quick(vector<int>& arr,int start,int end)
{
	int mid = OneQuick(arr, start, end);
	if (mid - start > 1)						//左边至少有两个元素
	{
		Quick(arr, start, mid - 1);              //递归处理前一段元素
	}
	if (end - mid > 1)							//右边至少有两个元素
	{
		Quick(arr, mid + 1, end);				 //递归处理后一段元素
	}
}

void QuickSort(vector<int>& arr)			//做一层简单的封装
{
	Quick(arr, 0, arr.size() - 1);
}
  • 非递归实现

处理思路:将Quick方法中的递归转化为非递归的方式。(利用循环实现)。其实整体思路是不变的,处理做部分的时候把有部分的start和end放到栈中保存。

void Quick2(vector<int>& arr, int start, int end)
{
	stack<int> st;
	st.push(end);
	st.push(start);
	while (!st.empty())
	{
		start = st.top(); st.pop();
		end = st.top(); st.pop();
		int mid = OneQuick(arr, start, end);
		if (mid - start > 1)
		{
			int start_l = start;
			int end_l = mid - 1;
			st.push(end_l);
			st.push(start_l);
		}
		if (end - mid > 1)
		{
			int start_r = mid + 1;
			int end_r = end;
			st.push(end_r);
			st.push(start_r);
		}
	}
}

直接插入排序

1.算法描述
  假如是这一组数据 83 15 35 1 33 8 77 21 34 97 31 2 9;
  首先把数据分成两个部分,前面的数据是有序数据段(初始时只有一个数据),后面的是无序数据段。

然后,从无序数据段的第一个元素(15)开始遍历,让它与有序数据段的最后一个元素进行比较(有序数据段的最后一个元素是最大(如果按增序)),如果比有序数据段最后一个元素要小,说明这个元素的位置还在前面(有序数据段已经是有序的了),所以用临时变量tmp保存15,让83移到15的位置。
所以第一次插入排序之后变成这个样子:
2.动画演示

3.代码描述

void InsertSort(vector<int>& arr)
{
	int len = arr.size();
	for (int i = 1; i < len; i++)    //遍历无序数据段
	{
		int tmp = arr[i];
		int j = i - 1;
		for (; j >= 0; j--)          //无序数据段与有序数据段进行比较
		{
			if ( tmp >= arr[j])      //如果条件满足就退出,减少遍历次数
			{
				break;
			}
			arr[j + 1] = arr[j];
		}
		arr[j + 1] = tmp;		     //将待排序数据插入到比它小的元素之后
	}
}

测试代码:

4.算法分析
时间复杂度:待排数据越有序,时间复杂度越低,如果数据完全有序,时间复杂度为O(n)–相当于遍历一遍数据。如果完全无序,时间复杂度为O(n^2);
空间复杂度:O(1);

希尔排序 – 缩小增量排序

 1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
1.算法描述

将待排数据分成n个组,每个组进行直接插入排序,目的就是使待排序列变得越来越有序。
接着将数据继续分组,分组越来越小,最后分组为1,一般每个分组数是互质的。
这两个步骤可以使用两个函数分别实现。

2.动画演示

3.代码展示

//Shell函数用于将每个分组排序
void Shell(vector<int>& arr, int group)    //和直接插入排序类似
{
	for (int i = group; i < arr.size() ; i++)    //吧i=1换成了i=group;
	{
		int j = i - group;
		int tmp = arr[i];
		for (; j >= 0; j -= group)                  //吧j--换成了j -= group
		{
			if (arr[j] <= tmp)
			{
				break;
			}
			arr[j + group] = arr[j];				 //吧j++换成了j += group
		}
		arr[j + group] = tmp;		
	}
}
//ShellSort用于减小分组的数据的个数
void ShellSort(vector<int>& arr)
{
	vector<int>  brr = { 5,3,1 };
	for (int i = 0; i < brr.size() ; i++)
	{
		Shell(arr, brr[i]);
	}
}

测试代码:

4.算法分析
时间复杂度:O(n^1.3)
空间复杂度:O(1)
稳定性:不稳定

简单选择排序

1.算法描述
想要达到的目的是:每次遍历一次,都要找到最小的元素放到最前面。
具体做法是:先遍历一遍整个待排序列,找到最小的元素与前面的第一个元素进行交换,重复上述过程,直到只有一个元素。
2.动画演示

3.代码展示

void SelectSort(vector<int>& arr)
{
	for (int i = 0; i < arr.size() - 1; i++)
	{
		int minIndex = i;
		for (int j = i + 1; j < arr.size(); j++)
		{
			if (arr[minIndex] > arr[j])  //如果最小的比j下标对应的元素还小,那么就把j作为最小的下标
			{
				minIndex = j;
			}
		}
		swap(arr[i], arr[minIndex]);
	}
}

堆排序

基本概念
:通常可以看做是一个完全二叉树
大根堆:这个完全二叉树的每一个根节点都大于它的左右孩子(如果有的话)
小根堆:这个完全二叉树的每一个根节点都小于它的左右孩子(如果有的话)
二叉树的存储方式
顺序存储:存放在数组中。
如果父节点的下标为i,则左右孩子下标分别为2i+1,2i+2
如果孩子结点的下标为j,则父节点的下标为(j-1)/ 2
链式存储:存储在链表中,每个节点至少有三个域(data,leftchild,rightchild)

1.算法描述
总的过程

  1. 先将元素构建成为一个大根堆 – CreateHeap()
  2. 交换元素(第一个与最后一个进行交换,这样就可以保证最后一个是最大的值,因为大根堆中第一个根节点一直是最大的,这个过程要执行len-1次)
  3. 每次交换完元素之后要进行一次大根堆的调整,来保证最大的元素在大根堆的根节点。用于下次与“最后一个元素进行交换” OneAdjust();
void HeapSort(vector<int>& arr)
{
	int len = arr.size();
	CreateHeap(arr);
	for (int i = 0; i < len - 1; i++)  //需要交换的次数
	{
		swap(arr[0], arr[len - 1 - i]);   //第一个与最后一个进行交换,保证最后一个元素是最大的
		
		OneAdjust(arr, len - 1 - i,0);    //调整一次
	}
}

创建大根堆
从最后一个根节点开始,进行调整,根节点每次减一,直到根节点为整个大根堆的根节点。
经过这个函数的执行,所有数据就被构建成了一个大根堆。

void CreateHeap(vector<int>& arr)
{
	int len = arr.size();
	int LastRoot = (len - 1 - 1) / 2;     //最后一个根节点的坐标

	for (int i = LastRoot; i >= 0; i--)
	{
		OneAdjust(arr,len,i);
	}
}

调整大根堆

  1. 先用i指向子树的根节点,j为根节点的左孩子。将i的位置保存到tmp中。
  2. 然后在左右孩子中找最大的值与tmp进行比较
  3. 如果条件满足替换i位置为j位置的值
void OneAdjust(vector<int>& arr, int len, int root)
{
	int tmp = arr[root];
	int i = root;
	int j = 2 * i + 1;

	while (j < len)
	{
		if (j+1 < len && arr[j] < arr[j + 1]) j++;

		if (arr[j] < tmp) break;   //必须是tmp,因为arr[j]没有被替换,还是原来的值

		arr[i] = arr[j];
		i = j;
		j = 2 * i + 1;
	}
	arr[i] = tmp;          //给tmp找到自己的位置

}

函数的目的:把以root为根节点的整个堆进行调整。这个函数是对建立好的大根堆由于改变一个结点所进行的调整,前提是左右孩子都是大根堆

2.代码展示

void OneAdjust(vector<int>& arr, int len, int root)
{
	int tmp = arr[root];
	int i = root;
	int j = 2 * i + 1;

	while (j < len)
	{
		if (j+1 < len && arr[j] < arr[j + 1]) j++;

		if (arr[j] < tmp) break;   //必须是tmp,因为arr[j]没有被替换,还是原来的值

		arr[i] = arr[j];
		i = j;
		j = 2 * i + 1;
	}
	arr[i] = tmp;          //给tmp找到自己的位置

}
void CreateHeap(vector<int>& arr)
{
	int len = arr.size();
	int LastRoot = (len - 1 - 1) / 2;     //最后一个根节点的坐标

	for (int i = LastRoot; i >= 0; i--)
	{
		OneAdjust(arr,len,i);
	}
}
void HeapSort(vector<int>& arr)
{
	int len = arr.size();
	CreateHeap(arr);
	for (int i = 0; i < len - 1; i++)  //需要交换的次数
	{
		swap(arr[0], arr[len - 1 - i]);   //第一个与最后一个进行交换,保证最后一个元素是最大的
		
		OneAdjust(arr, len - 1 - i,0);                   //调整一次
	}
}


void Print(vector<int>& arr)
{
	for (auto& x : arr)
	{
		cout << x << " ";
	}
	cout << endl;
}
int main()
{
	vector<int> arr = { 1,4,2,6,8,3,7,9 }; Print(arr);
	HeapSort(arr);  Print(arr);
	return 0;
}

二路归并排序

算法思想:

  1. 将序列中的待排序列分为若干组,每个数字分为一组
  2. 将若干个组两两合并,保证合并后的组是有序的
  3. 重复第二步操作直到只剩下了一组,排序完成

主要就是将两个归并段的数据进行排序,排序完之后合并成大的归并段再与其他的归并段进行合并来达到排序的目的

动画演示

代码展示

//此函数只是在width状态下的一次归并排序
void Meger(vector<int>& arr, int width)
{
	int len = arr.size() ;
	int low1 = 0;
	int high1 = low1 + width - 1;  //第一段位置结束的下标
	int low2 = high1 + 1;
	int high2 = (low2 + width) > len  ? (len - 1) : (low2 + width - 1);  //必须判断防止low2越界

	vector<int> brr;
	int index = 0;
	
	while (low2 < len)        //保持有两个归并段
	{
		while (low1 <= high1 && low2 &

以上是关于八大排序算法的主要内容,如果未能解决你的问题,请参考以下文章

C语言实现八大排序算法

数据结构初阶第九篇——八大经典排序算法总结(图解+动图演示+代码实现+八大排序比较)

数据结构初阶第九篇——八大经典排序算法总结(图解+动图演示+代码实现+八大排序比较)

八大排序算法C语言过程图解+代码实现(插入,希尔,选择,堆排,冒泡,快排,归并,计数)

八大经典排序算法的代码实现

《糊涂算法》之八大排序——希尔排序