1.11排序

目录

插入排序

基本思想

程序实现(InsertSort)

希尔排序

基本思路

程序实现(ShellSort)

堆排序

基本思路

程序实现(HeapSort)

选择排序

基本思路

程序实现(SelectSort)

冒泡排序

基本思路

程序实现(BubbleSort)

快速排序

基本思路

程序实现(QuickSort)

归并排序

基本思路

程序实现(MergeSort)


插入排序

基本思想

可以将一组混乱无序的数字看作两个类别,一个是有序的序列,一个是无序的序列。排序开始前,有序的序列为该数组的第一个,其余的数字均属于无序的序列。接下来插入排序的核心思想就是:将无序序列中的数字,逐个拿出,插入有序序列中,直至无序序列没有数字,即为排序完成。

时间复杂度为:O(n ^ 2)

程序实现(InsertSort)

void InsertSort(int* arr, int len)
{
	int i = 0;
	while (i < len - 1)
	{
		//i表示的是有序序列中数字的个数
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			//[0,end]为有序序列,[end + 1,len - 1]为无序序列,将arr[end + 1]插入,使得[0,end + 1]有序
			if (arr[end] > tmp)
			{
				//将arr[end]向后移动一个位置
				arr[end + 1] = arr[end];
				end--;
			}
			if (arr[end] <= tmp)
			{
				arr[end + 1] = tmp;
				break;
			}
		}
		i++;
	}
}

希尔排序

基本思路

希尔排序是插入排序的优化,希尔排序的基本思路为:
1.先进行预排序,使数组接近于有序
2.再进行插入排序

 gap 的作用为:进行多次预排序,使数组尽可能趋于有序。gap越大,数字可以移动的幅度越大;gap越小,则说明数组趋于有序,当gap==1,即为插入排序。

时间复杂度:O(n^{1.3}) ~O(n ^ 2)

程序实现(ShellSort)

//希尔排序是插入排序的优化
//1.先进行预排序,使数组接近于有序
//2.再进行插入排序
void ShellSort(int* arr, int len)
{
	
	int gap = len;
	//gap == 1 插入排序
	//gap > 1 预排序
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		int i = 0;
		//间隔为gap的多趟数据同时排序
		for (i = 0; i < len - gap; i++)
		{
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (arr[end] >= tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				if (arr[end] <= tmp)
				{
					arr[end + end] = tmp;
					break;
				}
			}
		}
	}
}

堆排序

基本思路

堆的逻辑结构是一棵完全二叉树,物理结构是一个数组。我们可以通过计算数组下标从而判断其父子关系。

parent * 2 + 1 = leftchild

parent * 2 + 2 = rightchild

parent = (child - 1) / 2

堆可以分为两类,最大堆最小堆

最大堆

(大堆)

最大堆中所有的父亲都大于等于孩子根节点储存的值是最大值

最小堆

(小堆)

最小堆中所有的父亲都小于等于孩子根节点储存的值是最大值

接下来,我们以数组arr[] = {1,18,5,9,3,8,12,2}为例,进行分析。

如图,堆的物理结构是arr这个数组,其逻辑结构如图。

我们堆排序的前提是建堆(时间复杂度:O(n ^ 2),这里我们要将数组arr变成一个小堆。这里我们用到的算法是向下调整算法。

向下调整算法:前提是所给的根节点 root 的左右子树都是小堆,利用这个算法可以将 root 为根节点的二叉树转换为一个小堆。具体思路如图。

如图,根节点的左子树和右子树都是小堆,符合向下调整算法的前提。

基本步骤为:

接下来,我们对于数组arr[] = {1,18,5,9,3,8,12,2},它的根节点的左右子树并不满足是小堆的前提。但是我们可以知道,当一棵二叉树深度为2时,这个二叉树的左右子树必然是小堆(以为根节点下就是叶子节点,叶子节点一定是小堆)。所以我们可以从倒数一个非叶子节点的子树开始向下调整,这样子自下而上,就可以保证整个二叉树都变成小堆了。如图:

堆排序

升序建大堆,找最大值
降序建小堆,找最小值

 还是以数组arr[] = {1,18,5,9,3,8,12,2}为例,我们需要升序,所以应该建大堆。

由于最大堆的特性,根节点的值是数组中的最大值,所以我们可以利用这个特性,进行以下操作。

时间复杂度:O(n\log_{2}n )

程序实现(HeapSort)

void AdjustDown(int* arr, int len, int root)
{
	//向下调整算法
	//前提:左右子树均为大堆
	//取 min(leftchild, rightchild),和 parent 比较
	//如果 parent < min(leftchild, rightchild), 则交换位置
	int parent = root;
	//左孩子的下标
	int child = parent * 2 + 1;
	//左孩子和右孩子比大小
	while (child < len)
	{
		if (arr[child] < arr[child + 1] && child + 1 < len)
		{
			child += 1;
		}
		if (arr[parent] < arr[child])
		{
			Swap(&arr[parent], &arr[child]);
		}
		//因为左右子树是大堆,父节点比左右子树的根节点还要大,所以父节点一定是最大的
		else
		{
			break;
		}
		//迭代条件
		parent = child;
		child = parent * 2 + 1;
	}
}

void HeapSort(int* arr, int len)
{
	//建堆
	//向下调整算法的前提是左右子树均为大堆
	//所以我们可以从倒数一个非叶子节点的子树开始向下调整
	//因为叶子节点没有孩子节点,所以一定满足左右子树是大堆
	int i;
	for (i = (len - 2) / 2; i >= 0; i--)
	{
		AdjustDown(arr, len, i);
	}
	int end = len - 1;
	for (end = len - 1; end > 0; end--)
	{
		Swap(&arr[end], &arr[0]);
		AdjustDown(arr, end, 0);
	}
}

选择排序

基本思路

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全
部待排序的数据元素排完 。

如下动图所示

时间复杂度:O(n^2)

程序实现(SelectSort)

void SelectSort(int* arr, int len)
{
	int i = 0;
	for (i = 0; i < len; i++)
	{
		int end = i;
		int min = end;
		for (end = i; end < len; end++)
		{
			if (arr[end] < arr[min])
			{
				min = end;
			}
		}
		Swap(&arr[i], &arr[min]);
	}
}

冒泡排序

基本思路

将键值较大的记录向序列的尾部移动,或者将键值较小的记录向序列的前部移动。

如下图所示

时间复杂度:O(n^2)

程序实现(BubbleSort)

void BubbleSort(int* arr, int len)
{
	int i = 0;
	int end = len - 1;
	for (end = len - 1; end > 0; end--)
	{
		int flag = 0;
		for (i = 1; i <= end; i++)
		{
			if (arr[i - 1] > arr[i])
			{
				Swap(&arr[i - 1], &arr[i]);
				flag = 1;
			}
		}
		//如果某一趟并未进行一次交换,则说明该趟已经有序,不需要继续循环
		if (flag == 0)
		{
			break;
		}
}	

快速排序

基本思路

流程如图所示

一.选取基准值

一般来说,我们选取数组中第一个为基准值。但是,如果要排序的数组是有序的,时间复杂度为 O(n^2),我们可以采取三数取中的方法避免该情况:,即取数组中第一个数,最后一个数,和中间一个数的中间大小值作为基准值。 

//优化:三数取中
int GetMidIndex(int* arr, int left, int right)
{
	int mid = (left + right) / 2;
	if (arr[left] > arr[right])
	{
		if (arr[mid] > arr[left])
		{
			return left;
		}
		else if (arr[mid] < arr[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (arr[mid] < arr[left])
		{
			return left;
		}
		else if (arr[mid] > arr[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
}

二.单趟排序

1.挖坑法

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

如图所示,先进行第一趟的快速排序。

此时我们可以发现,基准值6将原序列分为两个部分,左子列的所有的数都小于基准值,右子列的所有数都大于基准值,且基准值已经放在了正确的位置。

int PartSort1(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);
	//将arr[begin]放置在正确的位置
	int begin = left;
	int end = right;
	int key = arr[begin];
	int pivot = begin;
	while (begin < end)
	{
		//end向前遍历,找小于基准数的值
		while (arr[end] >= key && begin < end)
		{
			end--;
		}
		arr[begin] = arr[end];
		pivot = end;
		//begin向后遍历,找大于基准数的值
		while (arr[begin] <= key && begin < end)
		{
			begin++;
		}
		arr[end] = arr[begin];
		pivot = begin;
	}
	arr[end] = key;
	return pivot;
}

2.左右指针法

如下图所示,还是以第一个数确立为基准值,begin,end指针同时移动:begin指向的值大于基准值时,停止;end指向的值小于基准值时,停止。交换arr[begin],arr[end]。直至begin,end相遇,停止循环,将此时二者指向的值和基准值(数组第一个数)交换位置。

int PartSort2(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);
	//将arr[begin]放置在正确的位置
	int begin = left;
	int end = right;
	int pivot = begin;
	while (begin < end)
	{
		while (arr[end] >= arr[pivot] && begin < end)
		{
			end--;
		}
		while (arr[begin] <= arr[pivot] && begin < end)
		{
			begin++;
		}
		Swap(&arr[begin], &arr[end]);
	}
	Swap(&arr[begin], &arr[pivot]);
	pivot = begin;
	return pivot;
}

 3.前后指针法(快慢指针法)

cur向后遍历,每当arr[cur]小于基准值时,先将prev向后遍历一位arr[cur]和arr[prev]交换位置。(注意先后顺序)

int PartSort3(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);
	int cur = left + 1;
	int prev = left;
	int pivot = left;
	while (cur <= right)
	{
		if (arr[cur] < arr[pivot])
		{
			prev++;
			Swap(&arr[cur], &arr[prev]);
		}
		cur++;
	}
	Swap(&arr[pivot], &arr[prev]);
	pivot = prev;
	return pivot;
}

三.分治递归

 接下来,我们只需要将[ 0, pivot - 1]和[ pivot + 1, len - 1]这两个子序列排序即可。

分治递归:只要左边界等于于右边界,则说明子序列中只有一个数,则排序完成(递归终止条件)

时间复杂度:

时间复杂度:O(n\log_{2}n )(有序的情况下时间最长O(n^2)

小区间优化:当数组被分割到一个比较小的数量时,可以采取直接插入排序,减少递归调用的次数,以缩短运行时间。

if (pivot - left < 10)
	{
		InsertSort(arr + left, pivot - left);
	}
	else
	{
		_QuickSort(arr, left, pivot - 1);
	}
	if (right - pivot < 10)
	{
		InsertSort(arr + pivot + 1, right - pivot);
	}
	else
	{
		_QuickSort(arr, pivot + 1, right);
	}

程序实现(QuickSort)

void QuickSort(int* arr, int len)
{
	_QuickSort(arr, 0, len - 1);
}

void _QuickSort(int* arr, int left, int right)
{
	if (right <= left)
	{
		return;
	}

	//int pivot = PartSort1(arr, left, right);
	//int pivot = PartSort2(arr, left, right);
	int pivot = PartSort3(arr, left, right);

	//_QuickSort(arr, left, pivot - 1);
	//_QuickSort(arr, pivot + 1, right);

	//小区间优化
	if (pivot - left < 10)
	{
		InsertSort(arr + left, pivot - left);
	}
	else
	{
		_QuickSort(arr, left, pivot - 1);
	}
	if (right - pivot < 10)
	{
		InsertSort(arr + pivot + 1, right - pivot);
	}
	else
	{
		_QuickSort(arr, pivot + 1, right);
	}
}

//优化:三数取中
int GetMidIndex(int* arr, int left, int right)
{
	int mid = (left + right) / 2;
	if (arr[left] > arr[right])
	{
		if (arr[mid] > arr[left])
		{
			return left;
		}
		else if (arr[mid] < arr[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (arr[mid] < arr[left])
		{
			return left;
		}
		else if (arr[mid] > arr[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
}

int PartSort1(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);
	//将arr[begin]放置在正确的位置
	int begin = left;
	int end = right;
	int key = arr[begin];
	int pivot = begin;
	while (begin < end)
	{
		//end向前遍历,找小于基准数的值
		while (arr[end] >= key && begin < end)
		{
			end--;
		}
		arr[begin] = arr[end];
		pivot = end;
		//begin向后遍历,找大于基准数的值
		while (arr[begin] <= key && begin < end)
		{
			begin++;
		}
		arr[end] = arr[begin];
		pivot = begin;
	}
	arr[end] = key;
	return pivot;
}

int PartSort2(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);
	//将arr[begin]放置在正确的位置
	int begin = left;
	int end = right;
	int pivot = begin;
	while (begin < end)
	{
		while (arr[end] >= arr[pivot] && begin < end)
		{
			end--;
		}
		while (arr[begin] <= arr[pivot] && begin < end)
		{
			begin++;
		}
		Swap(&arr[begin], &arr[end]);
	}
	Swap(&arr[begin], &arr[pivot]);
	pivot = begin;
	return pivot;
}
int PartSort3(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);
	int cur = left + 1;
	int prev = left;
	int pivot = left;
	while (cur <= right)
	{
		if (arr[cur] < arr[pivot])
		{
			prev++;
			Swap(&arr[cur], &arr[prev]);
		}
		cur++;
	}
	Swap(&arr[pivot], &arr[prev]);
	pivot = prev;
	return pivot;
}

非递归实现快速排序

递归存在一定的缺陷,我们以下面的程序为例

我们写一个函数 f(n),用于计算小于n的所有正整数的和。该函数就是使用了递归算法。

int f(int n)
{
	return n <= 1 ? 1 : f(n - 1) + 1;
}
int main()
{
    printf("%d\n", f(10000));
    return 0;
}

但是当我们计算f(10000)时,无法得出结果。

我们可以画出递归图理解:

所以,递归有一个致命缺陷,就是如果递归深度太深,可能会导致栈空间不够用,从而导致栈溢出。

在这里,我们将会借助数据结构的栈模拟递归过程,以增加可调用的空间。

void _QuickSort(int* arr, int left, int right)
{
	if (right <= left)
	{
		return;
	}

	int pivot = PartSort1(arr, left, right);

	_QuickSort(arr, left, pivot - 1);
	_QuickSort(arr, pivot + 1, right);
}

这是未进行三数取中优化的快速排序,接下来我们将会通过画出递归展开图的方式,理解快速排序的逻辑,并通过数据结构的栈模拟递归调用的过程。

程序实现:

我们将左区间和右区间放入一个结构体 r 中,而栈st存储该结构体。

struct range
{
	int left;
	int right;
};
  • 栈中存储的是无序的序列。
  • 我们每次取出栈顶的元素,进行单趟排序,即可将原来一个无序序列 [r.left, r.right] 分为 [r.left, pivot - 1] 和 [pivot + 1, r.right]两个无序序列
  • 如果新的无序序列只有一个值,则已经有序,不需要压入栈中;
  • 如果新的无序序列还有多个值,则说明无序,需要重新压入栈中。
  • 当栈中没有元素时,即完成排序操作。
//非递归实现快排:借助数据结构的栈模拟递归过程
void QuickSortNonR(int* arr, int len)
{
	ST st;
	StackInit(&st);
	struct range r;
	r.left = 0;
	r.right = len - 1;
	StackPush(&st, r);
	//如果栈不为空,则说明需要排序
	while (!StackEmpty(&st))
	{
		//取栈顶元素
		r = StackTop(&st);
		StackPop(&st);
		//单趟排序
		//将一个无序序列 [r.left, r.right] 分为 [r.left, pivot - 1] 和 [pivot + 1, r.right]两个无序序列
		int pivot = PartSort1(arr, r.left, r.right);
		struct range tmp;
		tmp.left = 0;
		tmp.right = 0;
		if (pivot + 1 < r.right)
		{
			tmp.left = pivot + 1;
			tmp.right = r.right;
			StackPush(&st, tmp);
		}
		if (r.left < pivot - 1)
		{
			tmp.left = r.left;
			tmp.right = pivot - 1;
			StackPush(&st, tmp);
		}
	}
	StackDestory(&st);
}

归并排序

基本思路

如图所示,将原序列依次二分,分到只有一个数时,进行层层有序合并。

 单趟排序:

前提:将两个有序的数组,合并成一个有序的数组。

具体步骤:当两个区间有序,同时遍历这两个序列,取二者中较小的值插入临时的数组tmp中,如果这两个序列有剩余,则将剩余的值直接插入tmp中。然后将tmp中的值复制给原数组。

详见:10.30链表进阶_zhangyuaizhuzhu的博客-CSDN博客

	//归并:两个有序序列归并到一个有序序列中
	int cur = left;
	int prev = mid + 1;
	int i = left;
	while (cur <= mid && prev <= right)
	{
		if (arr[cur] < arr[prev])
		{
			tmp[i++] = arr[cur++];
		}
		else
		{
			tmp[i++] = arr[prev++];
		}
	}
	while (cur <= mid)
	{
		tmp[i++] = arr[cur++];
	}
	while (prev <= right)
	{
		tmp[i++] = arr[prev++];
	}

时间复杂度: O(n\log_{2}n )

空间复杂度:O(n) 

程序实现(MergeSort)

以测试数组arr[] = { 10,6,7,1,3,9,4,2 }为例:

进行归并排序时,由递归展开图可知:

 该程序的思路类似于二叉树的后序遍历,后序遍历是对二叉树的叶子节点进行打印输出;而归并排序也类似,每次遍历到最小的区间,再进行排序操作。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值