2024年【数据结构】八大排序算法详解_数据结构排序算法

我们发现当所有的元素全部有序时,只需进行比较的 n-1 次,无需挪动元素,那也就是说,元素越接近有序,算法的效率就越高。
 最好时间复杂度O(n)(所有元素为顺序),最坏时间复杂度O(n^2)(所有元素逆序
 在整个排序的过程中没有借助任何空间,所以空间复杂度为 O(1)
 直接插入排序是稳定的。因为具有相同关键字值的元素必然插在具有同一值的前一个元素的后面,即它们之间的相对顺序不变。

1.2 希尔排序( 缩小增量排序 )

希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap个组,所有距离为 gap 的记录分在同一组内,并对每一组内的记录进行插入排序。然后对 gap 缩减,重复上述分组和排序的工作。当到达 gap==1时,所有记录在统一组内排好序。

在这里插入图片描述

对于希尔排序,我们将前几轮的排序称为预排序,目的就是为了让整个序列基本有序,然后再对全体元素完成一次直接插入排序。因为直接插入排序在元素基本有序的情况下效率很高。

代码实现:

void ShellSort(int\* array, int sz)
{
	int gap = sz;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = gap; i < sz; ++i)
		{
			int key = array[i];
			int j = i - gap;
			while (j >= 0&&key<array[j])
			{
				array[j + gap] = array[j];
				j -= gap;
			}
			array[j + gap] = key;
		}
	}
}

注意: 该算法在具体实现时并不是先对一个子序列完成所有插入排序操作,再对另一个子序列进行,而是从第一个子序列的第二个元素开始顺序扫描整个待排序序列,当前待排序元素属于哪一个子序列,就在它相应的的子序列中进行排序。因而各个子序列地元素将会轮流出现,即算法将在每一个子序列中轮流进行插入排序。

希尔排序总结:

在希尔排序开始时增量(gap)较大,分组越多,每组的元素越少,因此各组内直接插入较快。后来增量逐渐缩小,分组数逐渐减少,每组内的元素个数越多,直接插入越慢,但是越接近有序。
 时间复杂度在O(nlogn) ~ O(n^2) 之间,最好时间复杂度为O(n^1.3), 最坏时间复杂度为O(n^2)。
 空间复杂度为O(1)。
 希尔排序是一种不稳定的排序算法。

2. 选择类排序

选择类排序的基本思想是,在第 i 趟的序列中选择第 i 小的元素作为有序序列的第 i 个元素。该算法的关键就在于,如何从剩余的待排序序列中找出最小或最大的元素。

2.1 简单选择排序

动图演示:

在这里插入图片描述

基本思路:从第一个元素开始,从头向尾遍历,标记序列中最小的元素,待所有元素对比完之后与第一个元素交换;然后继续从第二个元素开始继续比较,直到完成最后只有两个元素的交换。

代码实现:

void SelectSort(int\* array, int sz)
{
	for (int i = 0; i < sz- 1; ++i)
	{
		// 找当前区间中最大元素的位置
		int maxPos = 0;
		for (int j = 1; j < sz- i; ++j)
		{
			if (array[j] > array[maxPos])
				maxPos = j;
		}
		//加一层判断,若最大元素就在待排序序列的末尾就无需交换
		if (maxPos != sz- i - 1)
		{
			Swap(&array[maxPos], &array[sz- i - 1]);
		}
	}
}

上面的代码在每一次遍历过程将最大值找到并将其放在待排序序列末尾,其实还可以对其进行优化,就是再一次的遍历过程中既找到最大值也找到最小值,并将这两个值放在合适的位置上。

简单选择排序代码优化:

void SelectSortOP(int\* array, int sz)
{
	int begin = 0, end = sz- 1;  // [begin, end]
	while (begin < end)
	{
		// 在[begin, end]区间中找最大和最小的元素
		int maxPos = begin, minPos = begin;
		int j = begin + 1;

		while (j <= end)
		{
			if (array[j] > array[maxPos])
				maxPos = j;

			if (array[j] < array[minPos])
				minPos = j;

			++j;
		}

		// 如果最大元素不在区间最后的位置
		if (maxPos != end)
			Swap(&array[maxPos], &array[end]);

		// 如果end位置存储的刚好是最小的元素,上面的交换就将最小的元素位置更改了---maxPos
		// 最小元素的位置发生了改变,则必须要更新minPos
		if (minPos == end)
			minPos = maxPos;

		// 如果最小元素不在区间起始的位置
		if (minPos != begin)
			Swap(&array[minPos], &array[begin]);

		++begin;
		--end;
	}
}

注意: 对简单选择排序的优化代码,一定要注意待排序序列的末尾刚好存放的最小元素,或者首部存放的是最大元素,在发生交换时就会出现错误。假设说末位存放着最小元素,当你先将最大元素交换到末位,此时最大元素已经就位,然后要将最小元素放在首位,可是最小元素的标记指向着末位,当你再完成最小元素的交换时就会发现把刚换过来的最大元素交换到了前面。所以说,一定要注意最大元素或最小元素标记的改变。

简单选择排序总结:

简单选择排序很容易理解,但是它的效率实在太低,很少使用。
时间复杂度为O(n^2)
空间复杂度为O(1)
简单选择排序是不稳定的

2.2 堆排序

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

想要完成堆排序,首先要通过向下调整算法将二叉树这样的数据结构建成一个堆,然后再利用堆删除的思想完成排序。

2.2.1 堆向下调整算法

假若现在给你一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整

int array[] = {27,15,19,18,28,34,65,49,25,37};

在这里插入图片描述

void Swap(HPDataType\* left, HPDataType\* right)
{
	HPDataType temp = \*left;
	\*left = \*right;
	\*right = temp;
}
void AdjustDown(Heap\* hp,int n,int parent)
{
	int child = parent \* 2 + 1; //调整结点的左孩子
	while (child < n) //当child大于结点个数时调整完毕
	{
		//判断是否有右孩子并且右孩子大于左孩子
		if (child + 1 < n&&hp->array[child] < hp->array[child + 1])
		{
			child += 1;
		}
		if (hp->array[child]>hp->array[parent])
		{
			Swap(&hp->array[child], &hp->array[parent]); //若孩子大于父亲则交换
			//继续向下调整继续判断
			parent = child;
			child = parent \* 2 + 1;
		}
		else
		{
			return;
		}
	}
}

2.2.2 堆排序

在堆建立完毕之后,利用堆删除思想来进行排序,建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

思路: 1)完成建堆,让其拥有父亲结点大于孩子结点的特性(或者父亲结点小于孩子结点)
      2)交换根结点与最后一个孩子结点,那么此时最大的结点就来到了堆的最后一位,将堆的元素个数减一,然后在从根结点(刚交换上去的结点)完成向下调整算法。《注意: 堆的顺序结构是用数组实现的,所以说将堆元素个数减一并不是将其删除,而是将其放在了数组的最后一个位置》
      3)一直持续到最后两个结点将其完成交换即可完成堆排序。

void Swap(HPDataType\* left, HPDataType\* right)
{
	HPDataType temp = \*left;
	\*left = \*right;
	\*right = temp;
}
void AdjustDown(int\* array, int root, int sz)
{
	// 用child标记parent较大的孩子,默认先标记parent的左孩子
	// 先标记左孩子的原因是:如果parent有孩子,parent一定是先有左孩子,然后才有右孩子
	int parent = root;
	int child = parent \* 2 + 1;
	while (child < sz)
	{
		// 找parent中较大的孩子:用parent左右孩子比较
		// 必须先保证parent的右孩子存在
		if (child + 1 < sz && array[child + 1] > array[child])
		{
			child += 1;
		}
		// 检测parent是否满足堆的性质
		if (array[parent] < array[child])
		{
			Swap(&array[parent], &array[child]);
			parent = child;
			child = parent \* 2 + 1;
		}
		else
		{
			break;
		}
	}

}

void HeapSort(int\* array, int sz)
{
	int root = sz - 2 / 2;
	// 1. 建堆
	// 注意从倒数第一个非叶子节点的位置开始使用堆调整,一直调整到根节点的位置
	for (int i = root; i >= 0; --i)
	{
		AdjustDown(array, i, sz);
	}
	// 2. 排序--->利用堆删除的思想进行排序
	for (int i = sz - 1; i > 0; --i)
	{
		Swap(&array[0], &array[i]);
		AdjustDown(array, 0, i);
	}
}

堆排序的时间代价主要花费在建堆和排序上;

建堆的时间复杂度:
在这里插入图片描述
因此:建堆的时间复杂度为O(n)。

排序的时间复杂度:
  首先要明白的是建好堆之后,然后通过堆删除的思想来排序。而堆删除的思路是怎样?先将根结点与最后一个结点交换,然后把根结点放在数组地末位,然后数组元素减一,再将刚换上去地结点去执行向下调整算法,直到将所有地元素都交换完毕。我们发现对于每一个元素都要执行一次向下调整算法,而向下调整算法向下走的是单支树,所以向下调整算法的时间复杂度为O(logn),那么对于n个节点来说,完成整个排序的时间复杂度就为O(nlogn)。

所以,堆排序的时间复杂度为O(n)+O(nlogn),因为大O的渐进表示法的表示,堆排序的时间复杂度为O(nlogn)
空间复杂度为O(1)
堆排序是一种不稳定的排序算法。

3. 交换类排序

交换类排序算法的基本思想为,将待排序序列的元素两两比较,只要发现逆序就进行交换,知道没有逆序为止。如果要将整个序列调整为递增序列,那么元素之间是递减关系就为逆序。冒泡排序和快速排序就是典型的交换类排序算法。

3.1 冒泡排序

动图演示:

在这里插入图片描述
  冒泡排序也叫做“相邻比逆法”,即在扫描待排序记录序列时顺次比较相邻两个元素大小,如果逆序就交换位置。如果以将序列调整成升序为例,则逆序为两个关键字是降序序列。

具体地,各趟排序过程如下:

第1趟比较第1和第2个元素,如果逆序就交换,再依次比较第2个和第3个元素、第3个和第 4 个…若是逆序则交換。经过该趟比较和交换,最大的数必然“沉到”最后一个位置。
 第2趟用同样的方法,在前面的 n-1 个元素中依次进行比较和交换,第2大的数“沉到”倒数第2个位置上。
 第 i 趟仍用同样方法,在剩下的 n-i+1 个元素中依次进行比较和交换,第 i 大的数“沉到”倒数第 i 个位置上。
 重复此过程,直到 i=n-1最后一趟比较完为止。

代码实现:

void BubbleSort(int\* array, int sz)
{
	// 控制冒泡的趟数
	for (int i = 0; i < sz- 1; ++i)  // -1的目的是可以少冒一趟,因为最后一次冒泡区间中只剩余一个元素
	{
		// 具体冒泡的方式:用相邻位置的元素进行比较,如果不满足条件,就进行交换
		// j:表示后一个元素的下标,j要取到最后一个元素
		for (int j = 1; j < sz- i; ++j)  // -1目的:j最多只能取到冒泡区间的倒数第二个元素
		{
			if (array[j-1] > array[j])
				Swap(&array[j], &array[j - 1]);
		}
	}
}

同样我们可以对上面的代码进行优化,优化的点在哪呢?就是万一在排序的过程中序列已经顺序了,那就没有必要再冒泡下去了,此时我们便设置一个标记 flag ,每次冒泡前将其设置为0,若在一趟冒泡中发生了元素交换,就说明元素并没有顺序,便将其改为1,若在一趟冒泡中没有发生任何元素的交换,就说明该序列顺序,那么 flag 就不会被更改,然后就直接退出最外层的循环,完成了冒泡排序。

void BubbleSortOP(int\* array, int sz)
{
	int flag = 0;
	for (int i = 0; i < sz- 1; ++i)  // -1的目的是可以少冒一趟,因为最后一次冒泡区间中只剩余一个元素
	{
		flag = 0;  // 该趟冒泡还没有比较,因此将falg设置为0
		for (int j = 1; j < sz- i; ++j)  // -1目的:j最多只能取到冒泡区间的倒数第二个元素
		{
			if (array[j - 1] > array[j])
			{
				Swap(&array[j], &array[j - 1]);
				flag = 1;  // 在该趟冒泡时区间还无序
			}
		}
		if (!flag)
			return;
	}
}

冒泡排序总结:

冒泡排序在最好的情况下是序列为顺序,那么外层循环只进行一次就结束整个排序过程,最好的时间复杂度位O(n),但在最差的情况下外层循环最多进行 n-1 次,每一次外层控制的内层循环进行 n-i 次,最坏的时间复杂度为O(n^2)
 空间复杂度:O(1)
 冒泡排序是一种不稳定的排序算法。

3.2 快速排序

快速排序是一种应用非常广泛的排序算法,从它的名字就可以看出它的排序效率是比较高的。在1962年Hoare提出了一种划分交换排序,由于它几乎最快的排序算法,所以就被称为“快速排序”。

快速排序采用了一种分治的策略,分治法的基本思想是将原问题分解为若干个规模更小的问题但结构与原问题相似的子问题,递归的解这些子问题,然后将这些子问题的解组合称为原问题的解。
  基于这样的思想,快速排序的基本思想为:取待排序元素序列中的某一元素作为基准值(我们假设取最左边的值为基准值),按照该基准值将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后在左右子序列重复该过程,直到所有元素都排列在相应位置上为止。可以看出快速排序的遍历规则与二叉树的前序遍历非常类似的

接下来将从以下几个方面来对快排做一个详细介绍:
在这里插入图片描述

3.2.1 hoare版本

动图演示:
在这里插入图片描述

基本思想:选择待排序序列最左边的元素为基准值key,左右标记分别指向序列的第一个元素和最后一个元素,先让右标记往左走,当找到比key小的数字停下来,然后再让左标记往右走,当找到比key大的数字停下来,交换左右标记对应的数字,然后继续这样的操作,直到左右标记指向同一个元素(左右标记相遇),然后再交换基准值key和左右标记指向的元素。
  这样快排的一轮排序就完成了,可以看到每当完成一轮排序总会有一个元素落位到它该在的位置

代码实现:

void swap(int\* left, int\* right)
{
	int temp = \*left;
	\*left = \*right;
	\*right = temp;
}
int Partion(int\* array, int begin, int end)
{
	int left = begin;
	int left = end;
	int keyi = left;//基准值
	while (left < right)
	{
		//右标记向左走,直到找到小于key的元素就停下
		while (left < right&&array[right] >= array[keyi])
		{
			--right;
		}
		//左标记向右走,直到找到大于key的元素就停下
		while (left < right&&array[left] <= array[keyi])
		{
			++left;
		}
		swap(&array[left], &array[right]);
	}
	//交换基准值key和左右标记指向的元素
	//加判断的原因在于key右边的元素都比它小,所以就没有必要进行交换操作
	if (end != left)
	{
		swap(&array[left], &array[keyi]);
	}
	//返回已经被放好元素的位置,也就是key最终所在的位置
	return begin;
}
void quicksort(int\* array, int left, int right)
{
	//排序的终止条件(左右标记指向同一个元素,也就是待排序序列只有一个元素)
	if (left >= right)
	{
		return;
	}
	int div = Partion(array, left, right);
	//排序刚放好位置元素的左半边
	quicksort(array, left, div - 1);
	//排序刚放好位置元素的右半边
	quicksort(array, div + 1, right);
}

3.2.2 挖坑法

动图演示:
在这里插入图片描述

基本思想:同样选择序列的最左边值为基准值,左右标记分别指向序列的第一个元素和最后一个元素,那个坑位就是key所在的位置,先让右标记向左走,当找到比key小的元素停下,然后将该元素放置到那个坑位,此时就有了一个新坑位,而刚才的坑位就被填补了,然后再让左标记向右走,当找到比key大的元素停下,然后将该元素放置到新坑位,直到左右标记共同指向一个位置,也就是指向了一个坑位,然后再把基准值放到那个坑位就结束了一次排序过程。
  和hoare版本一样,每当完成一轮排序总会有一个元素落位到它该在的位置

代码实现:

int Partion(int\* array, int begin, int end)
{
	int left = begin, right = end;
	int key = array[begin];
	while (left < right)
	{
		while (left < right && array[right] >= key)
		{
			--right;
		}
		if (left != right)
		{
			array[left] = array[right];
		}
		while (left < right && array[left] <= key)
		{
			++left;
		}
		if (left != right)
		{
			array[right] = array[left];
		}
	}
	array[left] = key;
	return left;
}
void quicksort(int\* array, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int div = Partion(array, left, right);
	quicksort(array, left, div - 1);
	quicksort(array, div + 1, right);
}

3.2.3 双指针(前后指针)

动图演示:

在这里插入图片描述

基本思想:同样选择序列的最左边值为基准值,cur指向第二个元素,prev指向第一个元素,。cur先向右移动,当遇到比key小的元素停下来,然后再与prev的下一个元素交换。一直持续这样的操作,直到cur将所有元素都遍历完,然后再将prev指向的元素与key交换。这样该序列也就被分为两块。在这过程中可以发现,cur和prev中间的元素都是比key大的元素。

代码实现:

int Partion(int\* array, int begin, int end)
{
	int key = array[begin];
	int cur = begin+ 1, prev = begin;
	while (cur <= end)
	{
		if (array[cur] < key && ++prev != cur)
		{
			Swap(&array[cur], &array[prev]);
		}
		++cur;
	}
	Swap(&array[prev], &array[begin]);
	return prev;
}
void quicksort(int\* array, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int div = Partion(array, left, right);
	quicksort(array, left, div - 1);
	quicksort(array, div + 1, right);
}

3.2.4 栈实现 (非递归)

上面三种快速排序算法都是以递归的方法实现的,下面这个方法借用栈来实现非递归的排序算法。
  第一步:将序列的左右标记分别入栈,然后进入循环(判断条件为栈是否为空),再将左右标记出栈;
  第二步:利用刚出栈的这两个参数(左右标记)使用排序算法 Partion(array, left, right),以key为基准,将序列分割称为两部分,左边比key小,右边比key大,然后返回key所在的位置;
  第三步:现在就有两个待排序的序列,它们之间隔着一个刚落好位置的key,将这两部分序列的左右标记分别入栈,要做到与快排同样的基本思想(前序遍历),我们得先将key的右半部分入栈,再入栈key的左半部分。
  最后,当key的左右两边都只有一个元素或没有元素时就停止入栈,那么再进入到循环判断条件时,因为栈空就退出循环,至此非递归的快排就结束了。

int Partion(int\* array, int begin, int end)
{
	int key = array[begin];
	int cur = begin+ 1, prev = begin;
	while (cur <= end)
	{
		if (array[cur] < key && ++prev != cur)
		{
			Swap(&array[cur], &array[prev]);
		}
		++cur;
	}
	Swap(&array[prev], &array[begin]);
	return prev;
}
void QuickSortNor(int\* array, int begin, int end)
{
	Stack s;
	StackInit(&s);
	StackPush(&s, begin);
	StackPush(&s, end);
	while (!StackEmpty(&s))
	{
		int right = StackTop(&s);
		StackPop(&s);
		int left = StackTop(&s);
		StackPop(&s);
		int keyi = Partion(array, left, right);
		if (keyi + 1 < right)
		{
			StackPush(&s, keyi + 1);
			StackPush(&s, right);
		}
		if (left < keyi - 1)
		{
			StackPush(&s, left);
			StackPush(&s, keyi - 1);
		}
	}
	StackDestroy(&s);
}

3.2.5 快排优化(三数取中法+小区间优化)
3.2.5.1 三数取中法

为什么要用到三数取中法来进行优化就是因为,万一选取的基准值刚好是该序列的最小值,那么在一趟排序完成之后并没有什么效果,只是那个基准值的位置刚好落位了,但是它右边的元素都没有任何变化,所以说,为了避免这样的情况发生,我们采取三数取中法来进行优化。
  三数取中法是怎样操作的呢?选序列的最左、右边和中间的三个值比大小,将那个中间值放在最左边作为基准值,然后再去执行排序算法。

代码实现

int GetMiddleIndex(int\* array, int left,int right)
{
	int mid = (left + right) / 2;
	if (array[left] < array[mid])
	{
		if (array[mid] < array[right])
			return mid;
		if (array[right] < array[left])
			return left;
		else
			return right;
	}
	else
	{
		if (array[left] < array[right])
			return left;
		if (array[right] < array[mid])
			return mid;
		else
			return right;
	}
}

3.2.5.2 小区间优化

想一下,快排的思想就是不断地分割小序列,然后再递归实现,它的每一层的递归次数以2倍的次数进行增长。当元素较多时以递归的方法实现是不错的,但是当序列元素较少时,再使用递归就没有必要了,我们可以选择使用其他的排序方法来实现小序列的排序。

void QuickSort(int\* array, int left, int right)
{
	if (right - left < 16)
	{
		// [left, right)区间中数据少到一定程度,使用插入排序来优化
		InsertSort(array + left, right - left);
	}
	else
	{
		int div = Partion(array, left, right);
		QuickSort(array, left, div);
		QuickSort(array, div + 1, right);
	}
}

将这两种优化算法应用到双指针排序算法中:

int GetMiddleIndex(int\* array, int left,int right)
{
	int mid = (left + right) / 2;
	if (array[left] < array[mid])
	{
		if (array[mid] < array[right])
			return mid;
		if (array[right] < array[left])
			return left;
		else
			return right;
	}
	else
	{
		if (array[left] < array[right])
			return left;


![img](https://img-blog.csdnimg.cn/img_convert/255b5bc76e077f47325c744d74907ebc.png)
![img](https://img-blog.csdnimg.cn/img_convert/fd2ce1fb69db10ec42d5532d2877e4cd.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

array, left, right);
		QuickSort(array, left, div);
		QuickSort(array, div + 1, right);
	}
}

将这两种优化算法应用到双指针排序算法中:

int GetMiddleIndex(int\* array, int left,int right)
{
	int mid = (left + right) / 2;
	if (array[left] < array[mid])
	{
		if (array[mid] < array[right])
			return mid;
		if (array[right] < array[left])
			return left;
		else
			return right;
	}
	else
	{
		if (array[left] < array[right])
			return left;


[外链图片转存中...(img-bjx3Dnc8-1714635762055)]
[外链图片转存中...(img-MGujbYwm-1714635762056)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值