排序-三路划分

技巧:先处理 内层 一次排序,在处理外面

直接插入排序

请添加图片描述
升序
最坏(遇到降序):O(N^2) 等差数列 1+2+3+…+(n-1) = (n^2-n)/2
最好(有序) O(N)

希尔排序

gap 任何数字/2都是=1
gap/3 +1 保证gap最后是1

gap是多少 就分了多少组,每组数据可能少一点,但是肯定能分成gap组,前提是gap<n,也就是希尔的gap/2,gap/3+1,如果gap>n是不够分成gap组,比如gap=4,n=3分不成
在这里插入图片描述

时间复杂度分析

在这里插入图片描述

外层while(gap>1)----LogN
里面gap 很大 可以算出精确的2n 认为是N
gap=1 n 认为是N
gap变小过程中 ,N的变化
在这里插入图片描述
这里面就无法算出精确的了
结论:N^1.3

//N^1.3
void ShellSort(int* a, int n)
{
	//gap>1 预排序
	//gap == 1 直接插入排序
	int gap = n;
	while (gap > 1)
	{
		//gap /= 2;
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;//tmp>a[end] 结束
				}
			}
			a[end + gap] = tmp;
		}
		//PrintArray(a, n);
	}

}

选择排序

最烂的排序

在这里插入图片描述

最坏:N^2 N N-2 N-4 … 等差数列
最好:N^2
N-2还是优化后的,遍历一遍可以选出最大最小
不优化每次选出最小,选出一个

//最坏:N^2  N N-2 N-4 ...等差数列
//最好:N^2
void SelectSort(int* a, int n)
{
	int left = 0, right = n - 1;
	while (left < right)
	{
		int min = left, max = left;
		for (int i = left + 1; i <= right; i++)
		{
			if (a[i] < a[min])
			{
				min = i;
			}
			if (a[i] > a[max])
			{
				max = i;
			}
		}
		Swap(&a[left], &a[min]);
		if (left == max)
		{
			max = min;
		}
		Swap(&a[right], &a[max]);

		left++;
		right--;
	}
	
}

堆排序

O(NlogN)

交换排序

交换排序(冒泡,快排) 是把数据进行交换,也就是Swap() 就像不能直接站到同学的脑袋上去

冒泡排序

如果不加flag优化 时间复杂度 O(N^2) 等差数列 N-1 N-2…1
优化 最好O(N) 最坏O(N^2)
在这里插入图片描述

void BubbleSort(int* a, int n)
{
	for (int j = 1; j <= n - 1; j++)
	{
		bool ExChange = false;
		for (int i = 0; i < n - j; i++)
		{
			if (a[i + 1] < a[i])
			{
				Swap(&a[i + 1], &a[i]);
				ExChange = true;
			}
		}
		if (ExChange == 0)
		{
			break;
		}
	}
	
}

快速排序-递归

快排的递归是一种先序

  1. hoare版本

在这里插入图片描述

快速排序单趟干了两件事

选出一个关键值/基准值key,把他放到正确的位置(最终排好序要蹲的位置)
左边的都比key小,右边的都比key大

在这里插入图片描述
key一般选最左或者最右
左边做Key 右边先走找小 左边后走找大,LR都找到就交换,直到LR相遇位置就是key应该排在的位置,把key和LR相遇位置交换,完成单趟排

为什么要右边先走找小,左边再走找大呢?
结论就是这样才能保证他们相遇时位置的值比Key小,或者就是key的位置

关键是发生交换后把小的换到左边
我们分类处理
在这里插入图片描述

LR相遇后找到keyi应该排在的位置,根据keyi下标分出2个区间,继续递归下去,如果够均匀就是满二叉树了,思想也是二叉树前序递归的思想
[begin, keyi - 1] keyi [keyi+1, end]

技巧:把QuickSort1的形参的区间换成begin 和 end 里面用left 和 right 去进行 ++ – 交换
最后再递归划分区间的时候 begin 和 end没变 正好符合区间划分 [begin, keyi - 1] keyi [keyi+1, end]
例如

// [begin, end]
void QuickSort(int* a, int begin, int end)
{
	// 区间不存在,或者只有一个值则不需要在处理
	if (begin >= end)
	{
		return;
	}

	int left = begin, right = end;
	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[keyi], &a[left]);
	keyi = left;

	// [begin, keyi-1] keyi [keyi+1, end]
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi+1, end);
}

当然图里面正好相反,也没什么区别,虽然left和right 动了,但是begin 和end 一开始记录了,也没问题

在这里插入图片描述

递归返回条件是选出keyi 分出的区间只有一个值或不存在的区间
在这里插入图片描述

时间复杂度

在这里插入图片描述

如果能均分 就是满二叉树 高度次logN*单趟每次如图所示
为什么是logN而不是log(N+1)?
因为当N非常大时,logN和log(N+1)没什么区别可以忽略
二叉树最后一层占了总二叉树一半的节点数
假设N=100W就有20层
排到最后一层时,之前一半数量的节点都排好了,相当于最后一层 单趟N-50w 还是100W的量级
在这里插入图片描述

每层N-1 N-3 …减到最后一层N-50W 还是N的量级 所以说减不了多少,一共logN层 每层认为还是N
那就是O(NlogN)

数据有序时间复杂度反而最坏

最坏的情况 有序 反而变坏了 时间复杂度 N^2 等差数列
此时空间复杂度 是 O(N) 开辟了N个栈帧,平时空间复杂度是高度次O(logN)的栈帧(空间复用)
在这里插入图片描述

左边做key 123… 右边找不到比key小 R一直往左走
右边做key 7 6 5 … 1 左边找大 右边找小 R一直往左走

//hoare
//O(NlogN)
void QuickSort1(int* a, int left, int right)
{
	if (left>=right)
		return;

	int begin = left, end = right;

	//随机选key 
	//int randi = left + (rand() % (right - left));
	//Swap(&a[left], &a[randi]);

	//三目取中
	int midi = GetMidNumi(a, left, right);
	if(midi != left)
		Swap(&a[midi], &a[left]);

	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[keyi], &a[left]);
	keyi = left;
	//[begin, keyi - 1] keyi [keyi+1, end]
	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi+1, end);
}

这就是hoare大佬研究出来的方法,其他方法都是这个大思想,单趟排出来的结果可能不一样,但是还是这个思想,选出key,左分小,右分大

  1. 挖坑法

挖坑法和hoare 选出来的数有区别 ,但还是左边比key小,右边比key大

主要变化是单趟,搞了一个临时变量,和坑位,右边找到小R就变成新坑位,左边开始找大找到了也变成新坑位,直到相遇之后把临时变量Key放进去,区间被分成[begin, hole - 1] hole [hole+1, end]

//挖坑法
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left, end = right;

	//三目取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		Swap(&a[midi], &a[left]);

	int key = a[left];
	int hole = left;

	while (left < right)
	{
		// 右边找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		// 左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;

	
	//[begin, hole - 1] hole [hole+1, end]
	QuickSort2(a, begin, hole - 1);
	QuickSort2(a, hole + 1, end);
}

在这里插入图片描述

  1. 前后指针版本(最好)最简洁,好理解

单趟排出来和Hoare单趟也不太一样,但还是那个大思想
把比key大的值往右翻,比key小的值,翻到左边
在这里插入图片描述

//前后指针法
int PartSort3(int* a, int left, int right)
{
	//三目取中
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
		Swap(&a[midi], &a[left]);

	int keyi = left;

	int prev = left;
	int cur = left + 1;
	while (cur <= right)//cur < right 外面是n-1 可能最后一个没比较
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;

	return keyi;

}

在这里插入图片描述

快排优化

  1. 小区间优化
    目的解决最后一层的递归次数过多,比如说下面左区间5个数排好递归了7次

要注意的就是插入排序的开始是a+left,还有闭区间要+1-[right-left+1]
会有一些提升,但是不明显,但是数据量加大后还是有一定作用的
我的理解是 虽然优化了后几层递归次数,但前面还是NlogN的时间复杂度,大概是半数数据

在这里插入图片描述

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	//小区间优化--直接使用插入排序-优化后几层递归
	if (right - left + 1 > 10)
	{
		int keyi = PartSort3(a, left, right);
		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi + 1, right);
	}
	else
	{
		InsertSort(a+left, right - left + 1);
	}
	
}

解决有序数据的快排变慢

如果有序1,2,3,4…,左边选key或者右边选Key导致不能均分,使得快排效率变慢
2. 随机选key

	//随机选key 
	int randi = left + (rand() % (right - left));
	Swap(&a[left], &a[randi]);

范围是[left,right-1]这里面随机抽出一个来做key,注意到区间不是right,我试了下,如果选right在有序的情况下,一样烂

这种仍然能选到有序比如123…还是能选到1,但是数据量大了,几率很小

  1. 三目取中(更科学)
    [left,right]直接选出mid和left和right之中中间值,这样即使有序也可以均分了
    之前写个三数取中 费半天劲,其实画画图就出来了
    先确定a b的大小关系(a > b)(a < b),在判断c的位置 如图所示 可能是在 b a 之间的三个位置,就是这两种情况,根据图就能方便的写出什么情况返回什么
    在这里插入图片描述
int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] > a[mid])
	{
		if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			if (a[mid] < a[right])
			{
				return right;
			}
			else
			{
				return mid;
			}
		}
	}
	else//a[left] < a[mid]
	{
		if (a[right] > a[mid])
		{
			return mid;
		}
		else
		{
			if (a[left] > a[right])
			{
				return left;
			}
			else
			{
				return right;
			}
		}
	}
}

三路划分

解决问题:

在这里插入图片描述

思路:

在这里插入图片描述

具体实现

在这里插入图片描述

int getMid(int* a, int left, int right)
{
	//int mid = (left + right) / 2;
	int mid = left + (rand() % (right - left));//范围0~right-1,解决leetcode排序特殊用例 增加随机性
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else // a[left] > a[mid]
	{
		if (a[right] > a[left])
		{
			return left;
		}
		else if (a[mid] < a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
}

//三路划分 新
void QuickSort4(int* a, int begin, int end)
{
	//区间不存在 或 只有一个值则不需要处理
	if (begin >= end)
		return;
	int midi = getMid(a, begin, end);
	if (midi != begin)
		Swap(&a[midi], &a[begin]);

	
	int left = begin, right = end;
	int cur = left + 1;
	int key = a[begin];
	while (cur <= right)
	{
		if (a[cur] < key)
		{
			Swap(&a[left], &a[cur]);
			left++;
			cur++;
		}
		else if (a[cur] == key)
		{
			cur++;
		}
		else  // if(a[cur] > key)
		{
			Swap(&a[cur], &a[right]);
			right--;//右边确定一个比key大的right就可以--了,下次在继续判断cur的值,cur是中转,换过来的值还是有3种可能
		}
	}
	//[begin,left-1],[left,right],[right+1,end]
	QuickSort4(a, begin, left - 1);
	QuickSort4(a, right + 1, end);
	
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值