常见排序算法解析

小明 2025-05-07 21:54:31 6

芝兰���于深林,不以无人而不芳;君子修道立德,不为穷困而改节

文章目录

  • 插入排序
    • 直接插入排序
    • 希尔排序
    • 选择排序
      • 直接选择排序
      • 堆排序
      • 交换排序
        • 冒泡排序
        • 快速排序
          • 优化
          • 挖坑法
          • 前后指针法
          • 非递归版
          • 归并排序
              • 递归
              • 非递归
              • 总结

                插入排序

                插入排序(Insertion Sort)是一种简单直观的排序算法,它的工作原理类似于我们打扑克牌时对手中的牌进行排序的方式。插入排序的基本思想是将一个待排序的元素插入到已经排好序的部分的适当位置,使得插入后的序列仍然有序。

                插入排序的基本步骤如下:

                1. 从第一个元素开始,认为第一个元素已经是排好序的部分。
                2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
                3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
                4. 重复步骤3,直到找到已排序的元素小于或等于新元素的位置。
                5. 将新元素插入到该位置后。
                6. 重复步骤2~5,直到所有元素都排序完成。

                插入排序是一种稳定的排序算法,其时间复杂度为 O(n2),其中 n 是待排序元素的数量。最好情况下,如果数组已经有序,时间复杂度为 O(n);最坏情况下,如果数组逆序,时间复杂度为 O(n^2)。其空间复杂度为 O(1),因为它仅使用了常量级的额外空间。

                插入排序在小规模数据或基本有序数据的排序中性能较好,但在大规模数据排序中通常不如快速排序、归并排序等高级排序算法。然而,在某些特定情况下,插入排序可能会比其他高级算法更加有效,因此它仍然是一种重要的排序方法之一。

                直接插入排序

                直接插入排序是一种简单直观的排序算法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。直接插入排序的实现过程通常如下:

                1. 初始状态:假设待排序的元素存储在一个数组中,初始时将第一个元素看作是已排序的序列,其余元素看作是未排序的序列。
                2. 遍历未排序序列:从第二个元素开始,依次将未排序的元素插入到已排序的序列中。
                3. 插入操作:对于未排序序列中的每个元素,依次与已排序序列中的元素比较,找到合适的位置并插入,使得已排序序列保持有序。
                4. 重复步骤2和3,直到所有元素都插入到已排序序列中,排序完成。
                bool insertSort(std::vector& arr, int gap = 1)
                {
                	int n = arr.size();
                	for (int i = 1; i = 0 && arr[j] > key) 
                		{
                			arr[j + 1] = arr[j];
                			--j;
                		}
                		// 将 key 插入到合适的位置
                		arr[j + 1] = key;
                	}
                	return true;
                }
                

                直接插入排序的时间复杂度取决于输入数据的初始状态。

                • 最好情况: 如果输入数组已经是有序的,那么每次比较只需要比较一次,时间复杂度为 (O(n))。
                • 最坏情况: 如果输入数组是逆序排列的,那么每次插入一个新元素时都需要比较和移动前面已排序部分的所有元素,时间复杂度为 (O(n2))。
                • 平均情况: 平均时间复杂度也是O(n2),因为在平均情况下,每次插入新元素的位置不确定,需要移动的元素个数与已排序部分的长度相关。

                  直接插入排序的空间复杂度主要取决于需要排序的数组本身,以及一些额外的辅助空间。

                  • 原地排序: 直接插入排序是一种原地排序算法,它不需要额外的辅助空间,空间复杂度为 (O(1))。
                  • 除数组本身外的空间: 在进行直接插入排序时,只需要使用常量级别的额外空间来存储临时变量和索引,因此空间复杂度较低。

                    综上所述,直接插入排序的时间复杂度主要取决于输入数据的初始状态,而空间复杂度相对较低,是一种简单但不够高效的排序算法。

                    希尔排序

                    希尔排序(Shell Sort)是一种基于插入排序的排序算法,它是由Donald Shell在1959年提出的,也称为“缩小增量排序”。

                    希尔排序的基本思想是将原始数组分成若干个子序列,在每个子序列中进行插入排序,然后逐步缩小增量(间隔),最终完成对整个数组的排序。希尔排序的关键在于选择增量序列,并且在每轮排序中,将数组中相隔增量的元素分成一组,对各组进行插入排序。

                    • 增量序列的选择对希尔排序的性能有较大影响,常用的增量序列包括希尔增量序列、Hibbard增量序列、Sedgewick增量序列等。
                    • 希尔排序是不稳定排序算法,因为在排序过程中存在跨越多个子序列的元素交换。

                      希尔排序的时间复杂度取决于增量序列的选择,最坏情况下的时间复杂度为 O(n2),平均情况下的时间复杂度介于 (O(nlog2n)) 和 O(n2) 之间。希尔排序是一种原地排序算法,它的空间复杂度为 (O(1)),即常数级别的额外空间。

                      希尔排序相比直接插入排序效率高的原因主要是因为它利用了直接插入排序的优点,并通过分组插入的方式,使得数组在初始阶段就变得部分有序。

                      1. 间隔插入: 希尔排序将整个数组按照一定的间隔分成若干个子序列,然后对每个子序列进行插入排序。通过这种方式,希尔排序可以在一定程度上解决直接插入排序中逆序对过多的问题,因为通过较大的间隔,可以更快地将较小的元素移到数组的前面。
                      2. 逐步优化: 希尔排序通过不断减小间隔,逐步缩小子序列的规模,最终实现整个数组的基本有序。这种分阶段的逐步优化可以使得数组在排序过程中逐渐趋于有序状态,从而减少了每次插入操作的移动次数。
                      3. 时间复杂度优化: 希尔排序的时间复杂度相比直接插入排序有所降低。尽管希尔排序的最坏时间复杂度仍然是 O(n2),但在一般情况下,由于数组在初始阶段就变得部分有序,因此实际的比较和移动操作会减少,从而提高了排序的效率。
                      算法步骤说明
                      确定增量序列选择一个增量序列,通常是 n/2, n/4, n/8,…, 增量序列的选择会影响到希尔排序的性能。
                      按增量进行分组根据选择的增量序列,将待排序的数组分成若干个子序列。
                      对各个子序列进行插入排序对每个子序列进行插入排序,这里的插入排序是根据增量值进行的,而不是对整个数组进行插入排序。
                      逐步减小增量重复步骤 2 和 3,直到增量为 最后一次排序使用增量为 1 的插入排序,这时候数组已经基本有序,插入排序的效率会比较高。
                      bool shellSort(std::vector& arr)
                      {
                      	int gap = arr.size();
                      	while (gap)
                      	{
                      		for (int i = 1; i = 0 && arr[j] > key)
                      			{
                      				arr[j + 1] = arr[j];
                      				j--;
                      			}
                      			arr[j + 1] = key;
                      		}
                      		gap /= 2;
                      	}
                      	return true;
                      }
                      

                      综上所述,希尔排序是一种简单而有效的排序算法,适用于中等大小的数据集合。尽管其时间复杂度并不是最优的,但在实际应用中,希尔排序的性能仍然比较可观,特别是对于某些特定的增量序列。

                      选择排序

                      直接选择排序

                      直接选择排序(Selection Sort)是一种简单直观的排序算法,它的思想是在未排序序列中找到最小(或最大)元素,将其放置在已排序序列的末尾,然后将未排序序列的第一个元素与已排序序列的最后一个元素交换位置,如此循环,直到整个数组排序完成。

                      1. 从待排序序列中选择最小(或最大)的元素,记下其索引。
                      2. 将最小(或最大)元素与待排序序列的第一个元素交换位置。
                      3. 在剩余的待排序序列中继续执行步骤1和步骤2,直到所有元素都被排序。

                      直接选择排序的时间复杂度是 (O(n^2)),空间复杂度是 (O(1))。它是一种不稳定的排序算法,因为在选择过程中可能会改变相同元素的相对位置。

                      bool selectSort(std::vector& arr)
                      {
                      	for (int i = 0; i  
                      

                      堆排序

                      堆排序(Heap Sort)是一种基于完全二叉树(堆)的排序算法,它利用了堆的性质进行排序。堆是一种特殊的树形数据结构,具有以下特点:

                      1. 在堆中,每个节点的值都大于等于(最大堆)或小于等于(最小堆)其子节点的值。
                      2. 堆是一个完全二叉树,即除了最底层之外,其他层都被完全填满,且最底层从左到右填入。

                      堆排序的基本思想是:

                      1. 将待排序的序列构建成一个堆(通常是最大堆)。
                      2. 交换堆顶元素(最大值)和堆的最后一个元素,然后重新调整堆。
                      3. 重复步骤2,直到整个序列有序。

                      堆排序的步骤:

                      1. 构建堆:从最后一个非叶子节点开始,依次向前构建最大堆。
                      2. 交换堆顶元素和堆的最后一个元素,并调整堆。
                      3. 重复步骤2,直到堆中只剩一个元素。

                      堆排序是一种原地排序算法,它的时间复杂度是 (O(n log n)),空间复杂度是 (O(1))。堆排序是一种不稳定的排序算法,因为在调整堆的过程中可能改变相同元素的相对顺序。

                      void adjustDown(std::vector& arr, int cur, int len)
                      {
                      	int leftChild = cur * 2 + 1;
                      	while (leftChild  arr[leftChild]) ? rightChild : leftChild;
                      		if (arr[maxChild] > arr[cur])
                      		{
                      			std::swap(arr[maxChild], arr[cur]);
                      			cur = maxChild;
                      			leftChild = cur * 2 + 1;
                      		}
                      		else break;
                      	}
                      }
                      void createMaxHeap(std::vector& arr)
                      {
                      	int len = arr.size();
                      	for (int i = len / 2; i >= 0; i--)
                      	{
                      		adjustDown(arr, i, len);
                      	}
                      }
                      bool heapSort(std::vector& arr)
                      {
                      	createMaxHeap(arr);
                      	for (int i = arr.size() - 1; i > 0; i--)
                      	{
                      		std::swap(arr[0], arr[i]);
                      		adjustDown(arr, 0, i);
                      	}
                      	return true;
                      }
                      

                      交换排序

                      冒泡排序

                      冒泡排序是一种简单直观的排序算法,它的基本思想是通过交换相邻元素多次对数组进行遍历,每次遍历都将最大(或最小)的元素交换到数组的末尾(或开始)。这个过程类似于气泡在水中上升的过程,故称为冒泡排序。

                      1. 从数组的第一个元素开始,依次比较相邻的两个元素,如果它们的顺序不正确(比如前面的元素大于后面的元素),则交换它们的位置。
                      2. 继续比较相邻的元素,直到整个数组被遍历一次。
                      3. 重复上述步骤,每次遍历都将最大(或最小)的元素交换到数组的末尾(或开始),并缩小待排序数组的范围。

                      冒泡排序的时间复杂度取决于数组的初始状态,最好情况下(数组已经有序)时间复杂度为 (O(n)),最坏情况下(数组逆序)时间复杂度为 (O(n^2)),平均时间复杂度也为 (O(n^2))。冒泡排序是一种原地排序算法,只需要常数级别的额外空间来存储临时变量,因此空间复杂度为 (O(1))。

                      冒泡排序是一种稳定的排序算法。在相邻元素相等时,不会改变它们的相对顺序,因此是稳定的。冒泡排序的实现较为简单,但由于其时间复杂度较高,在实际应用中并不常见,通常被更高效的排序算法所取代,比如快速排序、归并排序等。

                      bool bubbleSort(std::vector& arr)
                      {
                      	for (int i = 0; i  arr[j]) std::swap(arr[j - 1], arr[j]);
                      		}
                      	}
                      	return true;
                      }
                      

                      快速排序

                      快速排序(Quick Sort)是一种高效的排序算法,属于交换排序的一种。它的基本思想是通过分治法(Divide and Conquer)来实现排序。

                      1. 选择基准值: 从数组中选择一个元素作为基准值(pivot)。
                      2. 分区操作: 将数组中小于基准值的元素放置在基准值的左侧,大于基准值的元素放置在右侧,基准值则放置在正确的位置上。
                      3. 递归排序: 对基准值左右两侧的子数组进行递归排序,直到整个数组有序。
                      实现思路说明
                      选择基准值通常选择数组中的第一个元素、最后一个元素或者随机一个元素作为基准值。
                      分区操作使用两个指针,一个指向数组的起始位置,一个指向数组的末尾位置。通过交替移动这两个指针,并交换元素,将数组分为两个部分。
                      递归排序对分区后的左右子数组分别进行递归排序。

                      快速排序的平均时间复杂度为 (O(nlogn)),最坏情况下的时间复杂度为 (O(n^2)),最好情况下的时间复杂度也为 (O(nlog n))。快速排序的性能很大程度上取决于选取的基准值。快速排序是一种原地排序算法,它的空间复杂度主要取决于递归调用栈的深度,平均空间复杂度为 (O(log n)),最坏情况下空间复杂度为 (O(n))。

                      快速排序是一种不稳定的排序算法,因为在分区操作过程中,相同元素的相对位置可能会发生变化。

                      	int sort(std::vector& arr, int begin, int end)
                      	{
                      		int key = begin;
                      		while (begin = arr[key]) end--;
                      			while (begin  
                      

                      优化

                      在快速排序中使用三数取中和小区间直接插入排序的优化,可以提高快速排序的性能,尤其是在处理小规模数据时。三数取中法是一种选择基准值的方法,它可以避免选取到最坏情况下的基准值,从而提高快速排序的性能。具体步骤如下:

                      1. 从待排序数组的起始、中间和末尾位置选择三个元素。
                      2. 对这三个元素进行排序,并取其中间位置的元素作为基准值。

                      在快速排序中,当待排序的子数组规模较小时,直接插入排序的性能可能比快速排序更好。因此,可以在快速排序中添加一个判断条件,当子数组的规模小于某个阈值时,使用直接插入排序对子数组进行排序。

                      以下是结合三数取中和小区间直接插入排序优化的快速排序算法的代码:

                      	int getKey(std::vector& arr, int begin, int end)
                      	{
                      		int mid = (begin + end) / 2;
                      		if (arr[end] > arr[begin])
                      		{
                      			if (arr[mid] > arr[end]) return end;
                      			else if (arr[mid]  arr[begin]) return begin;
                      			else return mid;
                      		}
                      	}
                      	int sort(std::vector& arr, int begin, int end)
                      	{
                      		int cur = getKey(arr, begin, end);
                      		std::swap(arr[cur], arr[begin]);
                      		int key = begin;
                      		while (begin = arr[key]) end--;
                      			while (begin  
                      

                      在实际代码中,要根据具体情况选择合适的阈值来确定是否使用直接插入排序,以及合适的阈值大小。此外,三数取中法的选择也可以根据实际情况进行优化和改进。

                      挖坑法

                      挖坑法也是快速排序的一种优化方法,用于在分区操作中避免不必要的元素交换。其基本思想是通过找到一个“坑”,将基准值放入坑中,然后利用坑的位置来实现元素的移动和交换,从而实现分区操作。

                      1. 选择基准值:从待排序数组中选择一个元素作为基准值。
                      2. 分区操作:
                        • 将基准值保存到一个临时变量中,此时数组中留下了一个“坑”。
                        • 从数组的另一端开始(通常是右侧),找到一个小于基准值的元素,并将其填入坑中。
                        • 然后从数组的左侧开始,找到一个大于基准值的元素,并将其放入刚才填入的坑中。
                        • 重复上述过程,直到左右指针相遇。
                        • 最后将基准值放入最后一个坑中,此时基准值左侧的元素都小于基准值,右侧的元素都大于基准值。
                        • 递归排序:对基准值左右两侧的子数组进行递归排序。

                      挖坑法的优点是减少了元素的交换次数,因为每次交换都需要消耗额外的时间,通过找到“坑”来避免了多余的交换操作,提高了快速排序的效率。

                      int potholingSort(std::vector& arr, int left, int right)
                      {
                      	int cur = left, val = arr[left];
                      	while (left = val) right--;
                      		arr[cur] = arr[right];
                      		cur = right;
                      		while (left 
The End
微信