【数据结构】一文带你全面了解排序(下)——冒泡排序、快速排序、归并排序、计数排序

这篇具有很好参考价值的文章主要介绍了【数据结构】一文带你全面了解排序(下)——冒泡排序、快速排序、归并排序、计数排序。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

 

目录

一、常见排序算法的实现 

 1.1 交换排序

1.1.1 基本思想

1.1.2 冒泡排序 

1.1.3 快速排序

1.2 归并排序

1.3 非比较排序

二、排序算法复杂度及稳定性分析


 人总得为过去的懒惰而付出点代价!


一、常见排序算法的实现 

 1.1 交换排序

1.1.1 基本思想

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

1.1.2 冒泡排序 

详细内容见:冒泡排序链接

冒泡排序:

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)//趟数
	{
		int end = n - i - 1;
		for (int j = 0; j < end; ++j)//交换次数
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
			}
		}
	}
}

冒泡排序优化:【当第一趟进行交换的时候,没有进行交换,说明数组是有序的,那么就不需要进行后面几趟的冒泡了】 

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)//趟数
	{
		int exchange = 0;
		int end = n - i - 1;
		for (int j = 0; j < end; ++j)//交换次数
		{
			if (a[j] > a[j + 1])
			{
				exchange = 1;
				Swap(&a[j], &a[j + 1]);
			}
		}
		if (exchange == 0)
		{
			break;
		}
	}
}

把直接插入排序和优化后的冒泡排序进行比较: 如果顺序是有序的,两者是一样的;但是,如果是局部有序,或者接近有序,那么插入适应性和比较次数更少

1. 冒泡排序是一种非常容易理解的排序

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1)

4. 稳定性:稳定

1.1.3 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止


 单趟排序:选出一个key,一般是第一个数或者是最后一个数,排序之后要求左边的值都比key小,右边的值都比key大

将区间按照基准值划分为左右两半部分的常见方式有(单趟排序):

(1)hoare版本(霍尔)

key在左边(第一个数

先右边的right找比key小的数据,找到就停止,然后左边的left找比key大的数据,找到就停止,然后进行交换数据,一直到left和right相遇,将该位置的值和key进行交换

【因为,交换完之后【left是小的】,右面先走,所以相遇的位置一定是比key小的数字】

【相遇的情况只有两种,left主动和right碰面和right主动和left碰面,这两种情况,都是在比key小的位置停下来】

key在右边(最后一个数

先左边的left找比key大的数据,找到就停止,然后右边的right找比key小的数据,找到就停止,然后进行交换数据,一直到left和right相遇,将该位置的值和key进行交换

【因为,交换完之后【left是小的】,左面先走,所以相遇的位置一定是比key大的数字】

【相遇的情况只有两种,left主动和right碰面和right主动和left碰面,这两种情况,都是在比key大的位置停下来】

代码1展示

void PartSort1(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
}

细节问题:(1)【key在左面】如果right在走得时候,遇到左面的数字一直小于等于key,就会一直--,就会越界,所以加上left<right

(2)挖坑法

key在左边(第一个数

边首先,把key值保存,然后这个位置成为一个坑位,然后右边的right找比key小的数据,找到就填补坑位,此时右面的right形成一个坑位,需要左面的left找比key大的数据,找到后填补右面的坑位,一直到left和right相遇,然后key填补相遇时的坑位【左边做坑,右边先走;右边做坑,左边先走】

key在右边(最后一个数)也同理

和hoare相比,代码几乎一样,但是容易理解:(1)不用理解为什么相遇位置比key小(2)不需要理解左边做key,右边先走

代码2展示

int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int pit = left;
	while (left < right)
	{

		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[pit] = a[right];
		pit = right;
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[pit] = a[left];
		pit = left;
	}
	a[pit] = key;
	return pit;
}

(3)前后指针法

key在左边(第一个数

两个指针,一个prev,一个cur。刚开始 prev在keyi的位置,cur在keyi的下一个位置;cur找小,如果找到prev++,并交换prev和cur的值;【如果cur和prev++在同一个位置,就没有交换数据的必要:如果cur的第一个位置就是比key小,那么需要prev++,然后交换位置,但是在同一个位置,就没有必要】【while(cur <= right)】

prev和cur的关系:(1)cur还没有遇到比key大的值,prev紧跟cur,一前一后(2)cur遇到比key大的值,prev和cur之间间隔着一段比key大的值的区间。

代码3展示:

int PartSort3(int* a, int left, int right)
{
	int key = a[left];
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < key && a[cur] != a[++prev])//这个条件,只有前面条件符合才会走后面的条件
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[left], &a[prev]);//这里不能写&key,因为并不能改变a[left]的值,不要和局部变量交换
	return prev;
}

key在右面(最后一个数

两个指针,一个prev,一个cur。刚开始 prev在left-1的位置,cur在left;cur找小,如果找到prev++,并交换prev和cur的值;【如果cur和prev++在同一个位置,就没有交换数据的必要:如果cur的第一个位置就是比key小,那么需要prev++,然后交换位置,但是在同一个位置,就没有必要】【while (cur <= right - 1】

int PartSort4(int* a, int left, int right)
{
	int key = a[right];
	int prev = left - 1;
	int cur = left;
	while (cur <= right - 1)
	{
		if (a[cur] < key && a[cur] != a[++prev])//这个条件,只有前面条件符合才会走后面的条件
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[right], &a[++prev]);//这里不能写&key,因为并不能改变a[left]的值,不要和局部变量交换
	return prev;
}

整体排序:单趟排序之后,key已经放在正确的位置,不需要进行移动,此时只要保证左边有序,右面有序,就可以保证整体有序了【分治解决子问题】

代码展示:(hoare单趟排序+分治整体排序) 

//快速排序

int PartSort(int* a, int left, int right)
{
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	int keyi = PartSort(a, begin, end);
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN)  每次调用快速排序,都需要把所有元素进行遍历一遍

最好的情况:每次选key都是中位数O(N*logN)   最差的情况:每次选key都是最大的数字或者最小的数字O(N^2)  [有序或者接近有序]

3. 空间复杂度:O(logN)

4. 稳定性:不稳定


 快速排序优化 

 因为最坏的情况,所以可以对key进行优化,让key不是最大或者最小的数字:(1)随机选key(2)三个数字选不大也不小的那个数字,然后这个数字和left或者right位置的数据进行交换。

代码展示:

int GetMinIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[right] > a[mid])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

int PartSort1(int* a, int left, int right)
{
	int midi = GetMidIndex(a, left, right);
	Swap(&a[left], &a[midi]);
	int keyi = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
			right--;
		while (left < right && a[left] <= a[keyi])
			left++;
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

小区间优化:区间很小时,不再使用递归划分的思路,而是直接使用插入排序对小区间进行排序,减少递归调用

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;
	if (end - begin + 1 <= 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort1(a, begin, end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

快速排序非递归(栈)

void QucikSort(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort(a, left, right);
		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi -1);
		}
		if (right > keyi + 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, right);
		}
	}
	StackDestory(&st);
}

 递归改成非递归:(1)用循环(2)用栈【递归会有爆栈的风险】

1.2 归并排序

先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表【先让左右区间有序,再对两个区间归并】


一个数组分成左右两个数组,假设数组有序,两个有序数组,归并成一个有序数组。(取两个数组中小的数据,依此尾插到新的数组),但是这个数组是无序的,所以把数组一直分,一直分到一个数据或者没有数据,就可以认为是有序的,然后依次合并,然后这个数组就是有序的。【先分再合并】

代码展示:【递归】

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;//中间值
	_MergeSort(a, begin, mid, tmp);//左边有序
	_MergeSort(a, mid + 1, end, tmp);//右边有序
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int index = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}
	memcpy(a+begin, tmp+begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

(1)mid等于(第一个数据的下标+最后一个数据的下标)/2,分成的两个区间应该是【left,mid】【mid+ 1, right】,否则就会出现死循环。

(2)分割完再合并,分割完分别调用合并排序,  合并排序写在调用后面。【分割的两个数组都有序之后,再合并】【函数主框架,左面有序+右面有序+合并】

(3)归并完再把内容复制到原来的数组里。【归并的时候需要新的数组】

(4)类似于后序遍历

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

2. 时间复杂度:O(N*logN)   N个数据,logN层 

3. 空间复杂度:O(N)    时间复杂度是O(N + logN) 但是 logN可以忽略不计

4. 稳定性:稳定

代码展示:【非递归】

//归并排序
//非递归
void MergeSort2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			if (end1 >= n)
				end1 = n - 1;
			if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			// begin2没有越界, end2越界,修正end2即可
			if (begin2 < n && end2 >= n)
				end2 = n - 1;

			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
					tmp[index++] = a[begin1++];
				else
					tmp[index++] = a[begin2++];
			}

			while (begin1 <= end1)
				tmp[index++] = a[begin1++];

			while (begin2 <= end2)
				tmp[index++] = a[begin2++];
		}
		memcpy(a, tmp, n * sizeof(int));
		gap *= 2;
	}
	free(tmp);
}

首先gap为1进行归并,此时2个数据一组是有序的,然后gap = gap*2 = 2,判断gap是否>=n, 然后gap为2进行归并,此时4个数据一组是有序的,然后gap = gap*2 = 4判断gap是否>=n,然后gap为4进行归并……直到gap>=n的时候结束【gap是多少,就多少一组进行合并】

越界问题:begin1是不可能越界的(begin1是等于i的,i又是小于n的)end1、begin2、end2是可能越界的

(1)只有end2越界,进行修正,n-1

(2)begin2越界,那么end2也越界,此时归并的第二组数据都越界,那么begin2和end2就不需要修正,那么第二组区间不存在

(3)end1越界,进行修正,那么第二组区间不存在即可

时间复杂度:O(N*logN)           空间复杂度:O(N) 

调试小技巧:

// 条件断点
			if (begin1 == 8 && end1 == 9 && begin2 == 9 && end2 == 9)
			{
				int x = 0;
			}

当我们想要在某一个地方停止,但是比较麻烦,可以直接写一个条件断点,然后打一个断点即可

1.3 非比较排序

比较排序:直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序

非比较排序:(1)计数排序(2)基数排序、桶排序

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

计数排序代码展示:

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	for (int i = 1; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];
		if (a[i] > max)
			max = a[i];
	}
	int range = max - min + 1;
	int* countA = (int*)malloc(sizeof(int) * range);
	if (countA == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	memset(countA, 0, sizeof(int) * range);
	for (int i = 0; i < n; i++)
	{
		countA[a[i] - min]++;
	}
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (countA[i]--)
		{
			a[j++] = i + min;
		}
	}
}

首先找出数据的最大值的最小值,然后相减+1算出新数组的大小,然后新数组的值都赋值为0,然后遍历以前的数组,遍历的数字-min,就是新数组的下标,这个下标里面的值就+1,遍历完以前的数组之后,再把新的数组的值再返回以前的数组即可

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。(适用于数据范围集中)(适用于整数,负数也可以,其他类型不可以)

2. 时间复杂度:O(MAX(N,范围))

3. 空间复杂度:O(范围)

4. 稳定性:稳定

二、排序算法复杂度及稳定性分析

【数据结构】一文带你全面了解排序(下)——冒泡排序、快速排序、归并排序、计数排序,初阶数据结构,笔记,算法,数据结构,排序算法

【数据结构】一文带你全面了解排序(下)——冒泡排序、快速排序、归并排序、计数排序,初阶数据结构,笔记,算法,数据结构,排序算法文章来源地址https://www.toymoban.com/news/detail-600179.html

到了这里,关于【数据结构】一文带你全面了解排序(下)——冒泡排序、快速排序、归并排序、计数排序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 一文带你全面了解什么是自动化测试?

    目录 简介 自动化测试概述 自动化测试目标 自动化测试流程 1. 测试计划和设计 2. 测试脚本开发 3. 测试执行和管理 4. 测试维护和优化 自动化测试最佳实践 自动化测试工具和框架 结论 软件测试是软件开发过程中一个必不可少的环节。传统的软件测试方式通常是手动测试,即

    2024年02月16日
    浏览(44)
  • 【数据结构】一篇带你彻底了解栈

    栈:一种线性数据结构,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶 (Top), 另一端称为栈底 [Bottom]。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。即最后进入的元素最先被访问。 压栈:栈的插入操作叫做进栈/压栈

    2024年02月05日
    浏览(88)
  • 【数据结构】初步了解排序

      Yan-英杰的主页 悟已往之不谏 知来者之可追    C++程序员,2024届电子信息研究生 目录 1.排序的概念及其运用         1.1排序的概念           2.常见排序算法的实现         2.1插入排序         2.2希尔排序                问题:gap是多少合适?        

    2024年02月11日
    浏览(40)
  • 【数据结构】一文带你掌握二叉树的构造与应用

    PS : 前面我们已经详细介绍了二叉树的概念以及二叉树的遍历的概念等,一些详细概念知识点可以在下面链接中的博客查看。本文主要需要使用代码自己实现二叉树及应用。 二叉树的概念及遍历 二叉树是由一个节点一个个连接而成的,每个节点最多连接两个节点,所以每个节

    2024年02月08日
    浏览(44)
  • 【数据结构与算法】三个经典案例带你了解动态规划

    从表中我们可以看到,最大的公共子串长度为2,一共有两个长度为2的公共子串,分别是第一个字符串的第2个字符到第3个字符和第一个字符串的第3个字符到第4个字符,即 ba 和 ac 根据上面的方法,我们来用代码封装一下求取最大公共子串的函数 function publicStr(s1, s2) { // 创建

    2024年04月09日
    浏览(95)
  • 【数据结构】带你玩转排序:堆排序、希尔排序、插入排序、选择排序、冒泡排序、快排(多版本)、归并排序

    英杰社区 https://bbs.csdn.net/topics/617804998 目录 常见算法的实现         插入排序         希尔排序         堆排序         选择排序         冒泡排序         快速排序         Hoare版本         随机选Keyi               三数取中         挖坑法  

    2024年02月08日
    浏览(56)
  • 【数据结构】速速收藏,一文带你参透双向链表各接口实现

    目录 🥕前言🥕: 🌽一、双向链表概述🌽: 1.双向链表概念: 2.双向链表结构: 🍆二、双向链表接口实现🍆: 1.工程文件建立: 2.接口实现(本文重点): Ⅰ.双向链表初始化: Ⅱ.打印双向链表: Ⅲ.申请新节点: Ⅳ.双向链表尾插: Ⅴ.双向链表尾删: Ⅵ.双向链表头插

    2024年02月02日
    浏览(42)
  • 深入了解数据结构第四弹——排序(1)——插入排序和希尔排序

    前言: 从本篇开始,我们就开始进入排序的学习,在结束完二叉树的学习之后,相信我们对数据在内存中的存储结构有了新的认识,今天开始,我们将进入排序的学习,今天来学习第一篇——插入排序 目录 什么是插入排序? 一、直接插入排序 1、直接插入排序的实现 2、直

    2024年04月11日
    浏览(37)
  • 由浅入深带你了解数据结构中的二叉树

    1.树的概念及结构 1.1树的概念   树是一种非线性的数据结构,它是由n(n=0)个有限节点组成一个具有层次关系的集合。它的形状像一颗倒挂的树,因此我们把它叫做树 。其特点如下所示:   1.有一个特殊的节点,称为根节点,根节点没有前驱节点   2.除根节点外,其余节点被

    2024年04月26日
    浏览(36)
  • 【数据结构】 二叉树理论概念!一文了解二叉树!

    🎥 屿小夏 : 个人主页 🔥个人专栏 : 数据结构解析 🌄 莫道桑榆晚,为霞尚满天! 什么是二叉树?二叉树的组成构造是什么样的?我们将由浅入深,循序渐进的方式把二叉树给搞明白,让你彻底了解二叉树! 树是一种非线性的数据结构,它是由n(n=0)个有限结点组成一

    2024年02月05日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包