【数据结构】排序篇

news2024/10/5 12:43:42

排序

  • 一、排序的概念和应用
    • 1.1、排序的概念
    • 1.2、排序的应用
    • 1.3、常见的排序算法
  • 二、插入排序
    • 2.1、直接插入排序
    • 2.2、希尔排序
    • 3.1.直接选择排序
    • 3.2、堆排序
  • 四、交换排序
    • 4.1、冒泡排序
    • 4.2、快速排序
      • 4.2.1、hoare版本
      • 4.2.2、挖坑法
      • 4.2.3、前后指针版本
      • 4.2.4、快排非递归(利用栈)
      • 4.2.5、快排非递归(利用队列)
      • 4.2.6、三路并排
  • 五、归并排序
    • 5.1、归并排序递归
    • 5.2、归并排序非递归
  • 六、非比较排序
    • 6.1、计数排序
  • 七、总结排序算法稳定性和时间复杂度

一、排序的概念和应用

1.1、排序的概念

  1. 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
  2. 排序的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
  3. 内部排序:数据元素全部放在内存中的排序。
  4. 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2、排序的应用

排序在生活中随处可见,比如,商品的价格排序,中国各大高校的排名,福布斯2022全球富豪榜,高考分数的排名等等。
在这里插入图片描述

1.3、常见的排序算法

在这里插入图片描述

二、插入排序

插入排序基本思想:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

2.1、直接插入排序

在这里插入图片描述

直接插入排序时间复杂度为O(logN)的算法,它有着稳定和速度快的优点,缺点是比较次数越少,插入点后的数据移动就越多,特别是数据庞大的时候就需要大量的移动数据。

代码描述:

#include<iostream>
using namespace std;

void InsertSort(int* nums,int len)	//给出待排序数组和数组元素个数
{
	for (int i = 0; i < len - 1; ++i)	//循环遍历 i<len-1的原因是防止越界,在插入过程中 会把最后一个元素插入排序数组中 
	{
		int end = i;	//end的下标代表着已经排序好的数组下标
		int key = nums[end + 1];	//待排序的元素
		while (end >= 0)		//递增排序
		{
			if (key >= nums[end])	//如果待排序元素比已经排序数组的最大还大,就不用动
				break;
			else                          //如果比排序数组的最大元素小  那么就需要递归判断
			{
				nums[end + 1] = nums[end];	//直接把数组的最后一个元素后移一个位置,已排序数组范围变大,腾出一个位置
				end--;						//end--,缩小比较范围,让该元素继续跟前面的比较  直到跳出循环
			}
		}
		nums[end + 1] = key;	//把待排序元素插入数组中,end+1的原因是保持稳定性,不改变原数组

	}
}

//测试
int main()
{
	int nums[] = { 15,3,5,26,31,7,56,24,19,48,77 };
	InsertSort(nums, sizeof(nums) / sizeof(nums[0]));
	for (auto& n : nums)
	{
		cout << n << " "; //C++范围for,也可以用C语言遍历打印
	}
	cout << endl;
}

在这里插入图片描述

然后我们来简单分析一下时间复杂度、空间复杂度:

首先空间复杂度O(1),毫无疑问;
那么时间复杂度?
O (N ^ 2) 插入排序最坏的情况就是对逆序进行排序,那么每一趟都会挪动数据:
第一次:挪动1次 第二次:挪动2次 第三次:挪动3次 …… 第n-1次:挪动n-1次 很明显是个等差数列!
然后根据大O渐近表示法,时间复杂度就是O(N ^ 2); 那么既然最坏的情况我们都讨论了,最好的情况嘞!
最好的情况就是数据已经有序了,那么我们每一次都不需要挪动数据,只需要比较一次,就break掉了,所以最好的情况的时间复杂度就是O(N)这可是个非常不错的速度;
其实我们在认真想一下,如果数组越接近有序的话是不是插入排序的时间复杂度就越接近O(N)!!!
相反数组越接近逆序,时间复杂度就越接近O(N^2); 为此呢我们从这一点看出,插入排序是一个适应性比较强的排序算法!!

总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

2.2、希尔排序

在这里插入图片描述
排序思想:

希尔排序法又称缩小增量法。是直接插入排序的改进版本,先选定一个整数gap,将其值赋为数据量的个数,然后将数据分为以gap为间隔的组先进行预排序。

在这里插入图片描述
预排序的规则和直接插入排序很相似,只不过直接插入排序是每次将相邻的两个数据进行比较并插入,而希尔排序则是每次将下标为n和n+gap的两个数据进行比较并插入,每一趟比较完成之后,gap变为gap/2或者gap/3+1,直到gap=1循环结束。

当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序了。最后一趟经过gap==1的直接插入排序后,数组就成功变成了有序。

那么我们为什么要进行预排序呢?主要是想快一点将大数排到后面去,小数排到前面去;
gap越大,数组预排序过后越接近无序;
gap越小,数组预排序过后越接近有序;

希尔排序的时间复杂度并不是很好计算,因此有许多不同的书籍给出的结论都有所差异,大约在O(n1.25)~O(1.6*n1.25)
之间。这里我们就折中一下记作O(n1.3)

代码描述:

void ShellSort(int* a, int n)		//传入数组和数组元素个数
{
	int gap = n;	//记录gap初始化位元素个数
	while (gap > 1)
	{
		gap = gap / 3 + 1;	//也可以是  gap/=2;   最终gap会变成1 成为直接插入排序
		for (int i = 0; i < n - gap; i++)		//循环遍历 每个gap比较一次 比较 n-gap次
		{
			int end = i;	//记录i的位置
			int tmp = a[end + gap];//当前要比较的元素  提前记录便于后面插入到前面
			while (end >= 0)
			{
				if (a[end] > tmp)	//如果当前位置大  需要换
				{
					a[end + gap] = a[end];
					end -= gap;		//跳出循环 最后可能会多判断一次满足条件后走else跳出
				}
				else          //不需要换 跳出
				{
					break;
				}
			}
			a[end + gap] = tmp;		//替换,end前面减去了gap  现在end+gap 指向原本的i位置 
		}
	}
}
int main()
{
	int nums[] = { 15,3,5,26,31,7,56,24,19,48,77 };
	ShellSort(nums, sizeof(nums) / sizeof(nums[0]));
	for (auto& n : nums)		//C++范围for,也可以用C语言遍历打印
	{
		cout << n << " ";
	}
	cout << endl;
}

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:

总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^1.3)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

3.1.直接选择排序

选择排序(Selectionsort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
在这里插入图片描述

排序思想:
直接选择排序思想是对每个下标i,从i后面的元素中选择最小的那个和s[i]交换。

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。这里我们每次都选择出一个最大值和最小值,每次将最大值与待排序的区间的最前面的位置交换,将最小值与待排序的区间的最后面的位置交换。

这里我们需要注意的是:如果已经将最小值和待排序区间最前面的位置交换后,发现最前面的位置和最大值的位置发生了冲突,需要特殊判断并处理一下。
直接选择排序思想是对每个下标i,从i后面的元素中选择最小的那个和s[i]交换。

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。这里我们每次都选择出一个最大值和最小值,每次将最大值与待排序的区间的最前面的位置交换,将最小值与待排序的区间的最后面的位置交换。

这里我们需要注意的是:如果已经将最小值和待排序区间最前面的位置交换后,发现最前面的位置和最大值的位置发生了冲突,需要特殊判断并处理一下。

但是这里呢我们做一下小小的优化,我们一次性选择两个数出来,具体方案就是:
我们在[left,right]区间选出最小值的下标和最大值的下标将其分别和left位置值交换,right值交换,
然后缩小区间,left++,right–;当区间只剩下一个元素的时候我们就可以停止了!


void Swap(int* a, int* b) //交换元素
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void SelectSort(int* a, int n)	//待排序数组  数组元素个数
{
	int begin = 0, end = n - 1;	//两个指针  指向开始和结尾
	while (begin < end)		//循环条件
	{
		int maxi = begin;	//记录最大值下标
		int mini = begin;	//记录最小值下标
		for (int i = begin + 1; i <= end; i++) //循环
		{
			if (a[i] < a[mini])
			{
				mini = i;	//找到最小值下标
			}
			if (a[i] > a[maxi])
			{
				maxi = i;	//找到最大值下标
			}
		}
		Swap(&a[mini], &a[begin]);//交换最小值和begin位置
		if (begin == maxi)		//如果最大值在第一个会被交换到mini位置  需要更新最大值位置
			maxi = mini;
		Swap(&a[maxi], &a[end]);	//交换最大值和最后一个
		begin++;	//双指针移动
		end--;
	}
}


int main()
{
	int nums[] = { 15,3,5,26,31,7,56,24,19,48,77 };
	SelectSort(nums, sizeof(nums) / sizeof(nums[0]));
	for (auto& n : nums)		//C++范围for,也可以用C语言遍历打印
	{
		cout << n << " ";
	}
	cout << endl;
}

时间复杂度不用说了,O(N^2);
并且对于这个排序来说没有什么最坏和最优情况,不管是什么情况都会去遍历一遍!

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

3.2、堆排序

在这里插入图片描述

排序思想:

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

这里我们简单来分析一下堆排序的思路,每次取堆顶数据和最后一个数据进行交换,然后再将除了最后一个元素外的所有元素进行向下调整使其再次成为一个堆。直到待调整的堆中的元素个数为0。
在这里插入图片描述

代码描述:

//交换函数
void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//向下调整建大堆		
void AjustDown(int* a, int parent, int n)	//需要堆数组  知道父亲的下标 和 数组元素个数
{
	int child = parent * 2 + 1;		//找到父亲的左孩子下标

	while (child < n) //沿着编号为parent的这条路径向下调整
	{
		if (child + 1 < n && a[child + 1] > a[child])  //如果存在右孩子 同时 右孩子大于左孩子
			child++;						//我们要找到的是最大的孩子 child++
		if (a[parent] < a[child])			//如果父亲小于孩子
		{
			Swap(&a[parent], &a[child]);	//交换父亲和孩子
			parent = child;					//让孩子等于父亲 继续沿着parent调整
			child = parent * 2 + 1;			
		}
		else
		{
			break;		//跳出循环  原因是已经调整完毕 找到最大的值放在堆顶
		}
	}
}

//向下调整建小堆
void AjustDown1(int* a, int parent, int n)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] < a[child]) //在此处调整
			child++;
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}
//堆排序  降序  向下调整建立小堆
void HeapSort1(int* a, int n)
{
	//先建堆(小根堆)
	for (int i = (n - 2) >> 1; i >= 0; i--)
		AjustDown1(a, i, n);
	//堆排序
	int size = n - 1;
	while (size > 0)
	{
		Swap(&a[0], &a[size]);
		AjustDown1(a, 0, size);
		size--;
	}
}

//堆排序  升序  向下调整建立大堆
void HeapSort(int* a, int n)	//传入数组  和数组元素个数
{
	//先建堆
	for (int i = (n - 2) >> 1; i >= 0; i--)	//从最后一个元素的父节点开始调整建堆
	{
		AjustDown(a, i, n);		//向下调整建立大堆
	}
	//堆排序
	int size = n - 1;	//最后一个元素下标
	while (size > 0)	//循环判断  不断调整
	{
		Swap(&a[0], &a[size]);	//交换堆顶和最后一个
		AjustDown(a, 0, size);		//堆顶元素向下调整
		size--;					//size--  再次找到数组前面的元素  进行排序
								//每次排序找到最大的数 放在后面  前面的不断调整 找到剩下的最大是数字
	}
}


int main()
{
	int nums[] = { 15,3,5,26,31,7,56,24,19,48,77 };
	//HeapSort(nums, sizeof(nums) / sizeof(nums[0]));
	HeapSort1(nums, sizeof(nums) / sizeof(nums[0]));

	for (auto& n : nums)		//C++范围for,也可以用C语言遍历打印
	{
		cout << n << " ";
	}
	cout << endl;
}

堆排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

四、交换排序

4.1、冒泡排序

主要思想就是,每进行一次冒泡就将最大值或最小值冒到了数组最后一个位置,然后数组元素减减,因为我已经将最大值或最小值冒到最后面了,不需要再对最后一个元素进行冒泡了,因此我们的数组元素个数要-1,减的就是已经冒好的元素!
在这里插入图片描述

void BubbleSort(int*a,int n)
{
	for (int i = 0; i < n - 1; ++i)			//需要排序的趟数
	{
		int flag = 0;		//标记位
		for (int j = 0; j < n - i - 1; ++j)	//要排序的次数
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);	//交换满足升序
					flag = 1;			//设置标记位为1
			}
		}
		if (flag == 0)		//如果标记位仍为0,说明已经有序,不需要再比较了,直接退出
		{
			break;
		}
	}
}

这里我们稍微做了一点点优化,我们如果在某一次冒泡中,发现数组已经有序了,那么就不用再进行剩下的冒泡了,直接break掉就好了,为了实现这一操作,我们定义了标记参数

冒泡排序的特性总结:

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

4.2、快速排序

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

4.2.1、hoare版本

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


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

}

int main()
{
	int nums[] = { 6,1,2,7,9,3,4,5,10,8 };
	int len = sizeof(nums) / sizeof(nums[0]);
	for (auto& t:nums)
	{
		cout << t << " ";
	}
	cout << endl;
	//InsertSort(nums, len);
	//selectSort(nums, len);
	//ShellSort(nums, len);
	//HeapSort(nums, len);
	//BubbleSort(nums, len);
	QuickSort(nums, 0, len - 1);

	for (auto& t : nums)
	{
		cout << t << " ";
	}
	cout << endl;

}

单趟排序图解,如下:

在这里插入图片描述
如果以左边的left为key那么就要右边先找小
如果以右边的right为key那么左边就要先找大
原因

以排升序且选用最左边的值为key为例。要确保left与right相遇位置的值小于基准值,就必须让right先进行移动。

情况一: right先移动,停止后left进行移动与right相遇,相遇位置即为right的位置(必然比基准值小)

情况二: right先移动,right在找到比基准值小的值之前与left相遇,相遇位置是left所在的位置,该位置的值是上一轮交换过来的(必然比基准值小)

总结:

  1. 时间复杂度: O(NlogN)
  2. 空间复杂度: O(logN)
  3. 稳定性: 不稳定

4.2.2、挖坑法

挖坑法本质上与Hoare版本并无不同,只是从思想上而言更容易理解

图解如下:

在这里插入图片描述

void Swap(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

void QuickSort(int* arr, int begin, int end)
{
	if (begin >= end)return;

	int left = begin, right = end;
	int pivot = left;		//记录基准值的下标
	int key = arr[pivot];	//记录基准值是多少
	while (left < right)
	{
		//右边找小,放在左边
		while (left < right && arr[right] >= key) {
			--right;
		}
		arr[pivot] = arr[right];
		pivot = right;
		//左边找大,放在右边
		while (left < right && arr[left] <= key) {
			++left;
		}
		arr[pivot] = arr[left];
		pivot = left;
	}
	pivot = left;
	arr[pivot] = key;
	QuickSort(arr, begin, pivot - 1);
	QuickSort(arr, pivot + 1, end);
}



int main()
{
	int nums[] = { 6,1,2,7,9,3,4,5,10,8 };
	int len = sizeof(nums) / sizeof(nums[0]);
	for (auto& t:nums)
	{
		cout << t << " ";
	}
	cout << endl;
	//InsertSort(nums, len);
	//selectSort(nums, len);
	//ShellSort(nums, len);
	//HeapSort(nums, len);
	//BubbleSort(nums, len);
	QuickSort(nums, 0, len - 1);

	for (auto& t : nums)
	{
		cout << t << " ";
	}
	cout << endl;

}

4.2.3、前后指针版本

本质是相同的!!

在这里插入图片描述

void Swap(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}
void QuickSort(int*a,int begin,int end)
{
	if (begin >= end) return;
	int key = begin;		//基准值下标
	int prev = begin;		//前指针
	int cur = begin + 1;	//后指针
	while (cur <= end)		//退出条件 后指针大于end   前指针不动 后指针找小 找到后 prev++ 交换前后下标内的值
	{
		if (a[cur] < a[key] && ++prev != cur)	//避免没必要的交换
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;	
	}
	Swap(&a[key], &a[prev]);	//交换基准值和prev
	QuickSort(a, begin, prev - 1);
	QuickSort(a, prev+1, end);

}


int main()
{
	int nums[] = { 6,1,2,7,9,3,4,5,10,8 };
	int len = sizeof(nums) / sizeof(nums[0]);
	for (auto& t:nums)
	{
		cout << t << " ";
	}
	cout << endl;
	//InsertSort(nums, len);
	//selectSort(nums, len);
	//ShellSort(nums, len);
	//HeapSort(nums, len);
	//BubbleSort(nums, len);
	QuickSort(nums, 0, len - 1);

	for (auto& t : nums)
	{
		cout << t << " ";
	}
	cout << endl;

}

4.2.4、快排非递归(利用栈)

递归的方式容易造成栈溢出

栈的特点就是先进行后出:
而我们的单躺排序是对一个区间进行操作,因此我们需要往栈里面存入区间,取的时候也应该按照区间去取:
基本思想:

  • 先将待排序列的第一个元素的下标和最后一个元素的下标入栈。(区间入栈)

  • 当栈不为空时,读取栈中的信息(一次读取两个: left、right),然后进行单趟排序,排完后获得了key的下标,然后判断key的左序列和右序列是否还需要排序,若还需要排序,就将相应序列的区间入栈;若不需排序了(序列只有一个元素或是不存在),就不需要将该序列的信息入栈。

  • 反复执行步骤2,直到栈为空为止。


#include<iostream>
#include<stack>
using namespace std;
void InsertSort(int* nums, int n)	//插入排序  向已排序数组插入新元素
{
	for (int i = 0; i < n - 1; ++i)
	{
		int end = i;
		int key = nums[end + 1];
		while (end >= 0)
		{
			if (nums[end] > key)
			{
				nums[end + 1] = nums[end];
				--end;
			}
			else
			{
				break;
			}
		}
		nums[end + 1] = key;
	}
}

void QuickSortStack(int* nums, int left, int right)
{
	stack<int> st;
	st.push(right);
	st.push(left);

	while (!st.empty())
	{
		int left = st.top();
		st.pop();
		int right = st.top();
		st.pop();
		if (right - left + 1 < 10)//小区间优化
		{
			InsertSort(nums + left, right - left + 1);
			continue;
		}
		//挖坑单趟排序
		int begin = left, end = right;
		int pivot = begin;
		int key = nums[begin];
		while (begin < end)
		{
			//右边找小,放在左边
			while (begin < end && nums[end] >= key)
			{
				--end;
			}
			nums[pivot] = nums[end];
			pivot = end;
			//左边找大,放在右边
			while (begin < end && nums[begin] <= key)
			{
				++begin;
			}
			nums[pivot] = nums[begin];
			pivot = begin;
		}
		pivot = begin;
		nums[pivot] = key;

		if (right > pivot + 1)
		{
			st.push(right);
			st.push(pivot + 1);
		}
		if (left < pivot - 1)
		{
			st.push(pivot - 1);
			st.push(left);
		}
	}
}



int main()
{
	int nums[] = { 6,1,2,7,9,3,4,5,10,8 };
	int len = sizeof(nums) / sizeof(nums[0]);
	for (auto& t:nums)
	{
		cout << t << " ";
	}
	cout << endl;
	//InsertSort(nums, len);
	//selectSort(nums, len);
	//ShellSort(nums, len);
	//HeapSort(nums, len);
	//BubbleSort(nums, len);
	//QuickSort(nums, 0, len - 1);
	QuickSortStack(nums, 0, len - 1);

	for (auto& t : nums)
	{
		cout << t << " ";
	}
	cout << endl;

}

4.2.5、快排非递归(利用队列)

#include<queue>
void QuickSortQueue(int* nums, int left, int right)
{
	queue<int>q;
	q.push(left);
	q.push(right);

	while (!q.empty())
	{
		int left = q.front();
		q.pop();
		int right = q.front();
		q.pop();

		if (right - left + 1 < 15)//小区间优化
		{
			InsertSort(nums + left, right - left + 1);
			continue;
		}

		//挖坑单趟排序
		int begin = left, end = right;
		int pivot = begin;
		int key = nums[begin];
		while (begin < end)
		{
			//右边找小,放在左边
			while (begin < end && nums[end] >= key)
			{
				--end;
			}
			nums[pivot] = nums[end];
			pivot = end;
			//左边找大,放在右边
			while (begin < end && nums[begin] <= key)
			{
				++begin;
			}
			nums[pivot] = nums[begin];
			pivot = begin;
		}
		pivot = begin;
		nums[pivot] = key;
		if (left <	pivot - 1)
		{
			q.push(left);
			q.push(pivot - 1);

		}
		if (pivot + 1 < right)
		{
			q.push(pivot+1);
			q.push(right);
		}		
	}
}



int main()
{
	int nums[] = { 6,1,2,7,9,3,4,5,8,10,15,24,18,34};
	int len = sizeof(nums) / sizeof(nums[0]);
	for (auto& t:nums)
	{
		cout << t << " ";
	}
	cout << endl;
	//InsertSort(nums, len);
	//selectSort(nums, len);
	//ShellSort(nums, len);
	//HeapSort(nums, len);
	//BubbleSort(nums, len);
	//QuickSort(nums, 0, len - 1);
	//QuickSortStack(nums, 0, len - 1);
	QuickSortQueue(nums, 0, len - 1);

	for (auto& t : nums)
	{
		cout << t << " ";
	}
	cout << endl;

}

三数取中:用来获取较为合适的key值

int GetMid(int* arr, int left, int right)//三数取中
{
	int mid = (left + right) >> 1;
	if (arr[left] < arr[mid])
	{
		if (arr[mid] < arr[right]) return mid;
		else if (arr[left] > arr[right]) return left;
		else return right;
	}
	else//arr[left] >= arr[mid]
	{
		if (arr[mid] > arr[right]) return mid;
		else if (arr[left] > arr[right]) return right;
		else return left;
	}
}

4.2.6、三路并排

其实上面的优化已经很好了,但是对于数组中的元素全是同一个元素这也的特例快排还是不能很好的解决,就算有上面的优化,此时快排的时间复杂度也会退化到O(N^2),为了解决这个问题大佬们提出了一个nb的算法,这个算法叫做三路并排,就是将数组分为上个区间:严格小于key的区间、等于key的区间、严格大于可以的区间:

在这里插入图片描述
算法的主要思路呢,是利用三个指针:left、right、cur;cur从left开始,然后key先保存一下left位置的值(也可以以右边作为基准值,那么key也就保存right所指的值),然后cur不断往后走;
1、如果cur位置的值小于key,那么就将cur此时所指的值往左边“甩”,即:交换left位置和cur位置的值,然后left++,cur++;
2、如果cur位置的值等于key,那么cur直接往后走;
3、如果cur位置的值大于key,交换cur位置的值和right位置的值;

void QuickSort3(int* nums, int left, int right)//三路并排(没有加三数取中、小区间优化)
{
	if (left >= right)
		return;
	int begin = left;
	int end = right;
	int cur = left;
	int key = nums[cur];
	while (cur <= right)
	{
		if (nums[cur] < key)
		{
			Swap(nums+cur , left + nums);
			left++;
			cur++;
		}
		else if (nums[cur] == key)
			cur++;
		else
		{
			Swap(nums + cur, nums + right);
			right--;
		}
	}
	QuickSort3(nums, begin, left - 1);
	QuickSort3(nums, right + 1, end);
}

总结:

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

五、归并排序

基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并;

5.1、归并排序递归

在这里插入图片描述
在这里插入图片描述
简单来说就是在两个有序区间之内选数,每次都选小的出来,放入临时数组中,当两个有序数组都选完过后,我们就直接将临时数组里面的元素在拷回原数组!
但是现在我们如何得到一个有序区间?
我们就将区间不断二分呗!当区间只剩下一个元素不就可以看成一个有序区间了?
代码实现:

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, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2) {
		//if (a[begin1] > a[begin2]) {//逆序
		if (a[begin1] < a[begin2]) {//顺序
			tmp[i++] = a[begin1++];
		}
		else {
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1) {
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2) {
		tmp[i++] = 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) {
		perror("malloc fail");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

总共logN层,每一层大概遍历N次,除了第一层不用归并之外,每一层的每个小区间都要归并,每一层的所有小区间合计起来就是O(N),因此logN层的时间复杂度就是O(NlogN)
所有归并排序的时间复杂度就是O(N
logN);
空间复杂度:这颗“树”的深度就是logN,也就是递归深度,我们要建立logN个栈帧,然后加上额外的临时空间O(N),所以准确的空间复杂度就是O(logN+N),然后大O渐进表示就是O(N);

5.2、归并排序非递归

归并排序的非递归的话,用如果还是像快排的非递归方式那样的话,是不行的也就是说单独用栈或队列的话完不成,主要是该递归属于“后序遍历”,我们归并完了这一层,但是我们找不到上一层,我们也就无法回到上一层,继续归并!如果我们能知道上一层的区间话,我们就能很好的对整个数组进行归并;为此我们需要从底层开始归并,我们可以设置一个gap,表示每个小区间的元素个数,然后我们两两一个大区间,(gap初始化为1)对这个大区间进行归并

void MergeSortNonR(int* a, int n) {

	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL) {
		perror("malloc fail");
		exit(-1);
	}
	//归并每组数据个数,从1开始,直接归并
	int rangeN = 1;
	while (rangeN < n) {
		for (int i = 0; i < n; i += rangeN * 2) {
			int begin1 = i, end1 = i + rangeN - 1;
			int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
			int j = i;
			//end1 begin2 end2越界
			if (end1 >= n) {
				//修正成不存在的区间 >归并完了拷贝
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
				//break;//跳出,必须每组归并拷贝才能用这种方法
			}
			else if (begin2 >= n) {
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n) {
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2) {
				//if (a[begin1] > a[begin2]) {//逆序
				if (a[begin1] < a[begin2]) {//顺序
					tmp[j++] = a[begin1++];
				}
				else {
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1) {
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2) {
				tmp[j++] = a[begin2++];
			}
			//归并一部分,拷贝一部分
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		//整体归并拷贝

		rangeN *= 2;
	}


	free(tmp);
	tmp = NULL;
}

六、非比较排序

6.1、计数排序

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整型数据。

//计数排序(适合范围集中的数据)(只适合整型) 
// 时间复杂度O(N+range)
//空间复杂度O(range)
void CountSort(int* a, int n) {
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++) {
		if (a[i] > max) {
			max = a[i];
		}
		if (a[i] < min) {
			min = a[i];
		}
	}
	int range = max - min + 1;
	int* countA = calloc(range,sizeof(int));//与malloc区别就是直接初始化为0
	if (countA == NULL) {
		perror("calloc fail");
		exit(-1);
	}
	// 1.统计次数,映射的方式,正负数均可统计
	for (int i = 0; i < n; i++) {
		countA[a[i] - min]++;
	}
	//2.排序
	int k = 0;
	for (int j = 0; j < range; j++) {
		while (countA[j]--) {
			a[k++] = j + min;
		}
	}
	free(countA);
}

时间复杂度:O(N + range)

空间复杂度:O(range)

稳定性: 对于只能排序整型的排序算法,无讨论是否稳定的必要性

七、总结排序算法稳定性和时间复杂度

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/622906.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图结构的原理

引言 胡图图:“我成为电脑砖家(人们都在我吧上评论电脑配置).,按理说我应该开一家图图计算机研究科技公司…”! 于小美:“没错,图图应该开一家公司 来扩展你的专业知识” 何壮壮:“厉害是厉害 ,要不要大哥来帮帮你(至于钱,好说:月薪2万)…” 图图:“你狮子大开口! ,那你还是当…

『赠书活动--第三期』清华社赞助 | 《Python系列丛书》

『赠书活动 &#xff5c; 第三期』 本期书籍&#xff1a;《Python系列丛书》 Python从入门到精通(微课精编版) PyTorch深度学习简明实战 Django Web开发实例精解 Python分布式机器学习 Python Web深度学习 618&#xff0c;清华社 IT BOOK 多得图书活动开始啦&#xff01;活动…

Vue.js 中的 keep-alive 组件使用详解

Vue.js 中的 keep-alive 组件 在 Vue.js 中&#xff0c;keep-alive 组件是一个非常有用的组件&#xff0c;它可以帮助我们优化页面性能。在本文中&#xff0c;我们将介绍 keep-alive 组件是什么&#xff0c;如何使用它以及它的作用。 keep-alive 组件是什么&#xff1f; keep…

C plus plus ——【模板应用】

系列文章目录 C plus plus ——【模板应用】 文章目录 系列文章目录前言一、函数模板1.1、函数模板的定义1.2、函数模板的作用1.3、重载函数模板 二、类模板2.1、类模板的定义与声明2.2、简单类模板2.3、默认模板参数2.4、为具体类型的参数提供默认值 三、总结 前言 模板是C语…

Selenium Python教程第4章

4. 查找元素 在一个页面中有很多不同的策略可以定位一个元素。在你的项目中&#xff0c; 你可以选择最合适的方法去查找元素。Selenium提供了下列的方法给你: find_element_by_id find_element_by_name find_element_by_xpath find_element_by_link_text find_element_by_par…

自己制作智能语音机器人(基于jetson nano)

1 简介 如上图&#xff0c;主要采用jetson上编写python代码实现&#xff0c;支持离线语音唤醒、在线语音识别、大模型智能文档、在线语音合成。 所需硬件如下&#xff1a; jetson nano&#xff1a;linux科大讯飞麦克风硬件&#xff1a;AIUI R818麦克阵列开发套件6麦阵列&#…

华为全栈自主数据库GaussDB正式面向全球服务

一、前言 在6月7日举行的华为全球智慧金融峰会2023上&#xff0c;华为发布新一代分布式数据库GaussDB&#xff0c;并正式向全球客户提供服务。据介绍&#xff0c;GaussDB实现了核心代码100%自主研发&#xff0c;是国内当前唯一做到软硬协同、全栈自主的国产数据库。 可谓是里…

继承类的方法

1 问题 定义一个父类&#xff0c;用子类去继承父类所拥有的方法、定义属性&#xff0c;然后使用测试文件实现子类输出父类的方法信息&#xff0c;属性等。 2 方法 2.1 定义一个名为Person的父类&#xff1a; 2.2 定义一个名为Student的子类&#xff0c;并令其继承父类&#xff…

【PXIE301-211】基于PXIE总线架构的16路并行LVDS采集、1路光纤数据处理平台

PXIE301-211是一款基于PXIE总线架构的16路并行LVDS数据采集、1路光纤收发处理平台&#xff0c;该板卡采用Xilinx的高性能Kintex 7系列FPGA XC7K325T作为实时处理器&#xff0c;实现各个接口之间的互联。板载1组64位的DDR3 SDRAM用作数据缓存。板卡具有1个FMC&#xff08;HPC&am…

20道常考Python面试题大总结,让你轻松拿下大厂offer

关于Python的面试经验 一般来说&#xff0c;面试官会根据求职者在简历中填写的技术及相关细节来出面试题。 一位拿了大厂技术岗Special Offer的网友分享了他总结的面试经验。当时&#xff0c;面试官根据他在简历中所写的技术&#xff0c;面试题出的范围大致如下&#xff1a; …

国际化语言项目

基本概念 1、使用QString对象表示所有用户可见的文本。由于QString内部使用Unicode编码实现&#xff0c;所以它可以用 于表示所有需要向用户呈现的文本。当然&#xff0c;对于仅程序员可见的文本并不需要都变为QString对象&#xff0c;可利 用Qt提供的QCString或原始的“char …

VisualGLM训练缺失latest文件问题解决

清华已经公布了VisualGLM 模型&#xff0c;图像预测也取得了比较好的效果&#xff0c;但是我在调试微调的过程遇到不少问题&#xff0c;这里记录一下缺失latest问题解决&#xff08;ValueError: could not find the metadata file ../latest, please&#xff09; 修正后的代码可…

PyEMD算法解析

算法背景 经验模态分解&#xff08;Empirical Mode Decomposition&#xff0c;缩写EMD&#xff09;是由黄锷&#xff08;N. E. Huang&#xff09;在美国国家宇航局与其他人于1998年创造性地提出的一种新型自适应信号时频处理方法&#xff0c;特别适用于非线性非平稳信号的分析处…

易基因|一种全新的检测DNA羟甲基化的技术:ACE-Seq

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑做组学科研服务的易基因。今天给大家介绍一种全新的检测DNA羟甲基化的技术&#xff1a;APOBEC-coupled epigenetic sequencing&#xff0c;简称【ACE-seq】。 前言 DNA序列中胞嘧啶&#xff08;C&#xff09;5’ 碳…

sed命令对文件内的指定字符串进行替换

目录 一、创建一个txt文件 二、替换每行第一个huawei为apple&#xff0c;第三个“/”后&#xff0c;不加参数就是只替换第一个 三、替换每行所有的xiaomi为iphone&#xff0c;第三个“/”后&#xff0c;加参数g就是替换所有 四、替换每行第二个redmi为potato&#xff0c;第…

ubutun22.04使用deb包安装mysql8.0.33

下载:https://dev.mysql.com/downloads/mysql/ 下载完毕,在ubuntu服务器解包。 安装使用dpkg命令,依次执行如下: sudo dpkg -i mysql-common_8.0.33-1ubuntu22.04_amd64.deb sudo dpkg -i mysql-community-client-plugins_8.0.33-1ubuntu22.04_amd64.deb sudo dpk…

云原生|秒懂云原生容灾备份实践

作者&#xff1a;刘健 后端开发工程师 目录 一、需备份的数据 二、在云航项目中使用 三、备份任务说明 一、需备份的数据 kubernetes在运行中&#xff0c;通常会产生两类数据&#xff1a; kubernetes集群资源对象数据。 容器运行时产生的数据。 针对cloudUp项目而言&am…

淘宝商品信息存入数据库

python 爬虫程序&#xff1a; #京东.pyimport json import pprint import re import requests # name_turnover {} url "https://s.taobao.com/search?data-keys&data-value88&ajaxtrue&_ksTS1686118766568_2290&callbackjsonp2291&ieutf8&in…

用AI写出的高考作文!

今天是6月7日&#xff0c;又到了每一年高考的日子。小灰自己参加高考是在2004年&#xff0c;距离现在已经将近20年&#xff0c;现在回想起来&#xff0c;真的是恍如隔世。 今天高考语文的作文题是什么呢&#xff1f; 全国甲卷的题目是&#xff1a;人技术时间 人们因技术发展得以…

centos7 部署 Redis

从源安装Redis 一、安装Redis1.1 下载源文件1.2 编译源文件1.2.1 解压文件1.2.2 编译Redis 1.2.3 安装Redis1.2.4 启动 Redis 二、Redis设置2.1 缓存设置2.2 redis 环境优化2.3 安全设置 一、安装Redis 1.1 下载源文件 使用下列命令获取最新版的稳定Redis wget https://down…