比较之舞,优雅演绎排序算法的智美篇章

news2025/1/16 5:52:19

大家好,这里是小编的博客频道
小编的博客:就爱学编程

很高兴在CSDN这个大家庭与大家相识,希望能在这里与大家共同进步,共同收获更好的自己!!!

本文目录

  • 引言
  • 正文
    • 一、冒泡排序:数据海洋中的升腾之光
      • 1、定义与原理
      • 2、算法步骤
      • 3、性能分析
      • 4、优化方案
      • 5、适用场景
    • 二、选择排序:万千数据中的最优寻觅之旅
      • 1、什么是选择排序
      • 2、选择排序的工作原理
      • 3、选择排序的具体步骤
      • 4、C语言实现选择排序
      • 5、选择排序的优缺点
        • (1)优点
        • (2)缺点
      • 6、选择排序的应用场景
    • 三、堆排序:肩担比较算法里的秩序之责
      • 1、概述
      • 2、基本思想
      • 3、实现步骤
        • (1). 构建初始堆
      • (2). 调整堆并排序
      • 4、代码示例
      • 5、优缺点分析
        • (1)优点
        • (2)缺点
      • 6、总结
    • 四、插入排序:简单却强大的数据整理工具
      • 1、概述
      • 2、算法步骤
      • 3、示例
      • 4、时间复杂度分析
    • 五、希尔之光:跨越间隔的优雅排序之旅
      • 1、引言
      • 2、希尔排序的原理
      • 3、希尔排序的实现步骤
      • 4、希尔排序的优缺点
        • (1)优点
        • (2)缺点
    • 六、快速排序:速度与效率的完美平衡
      • 1. 基本快速排序
        • 霍尔法(Hoare法)
        • 挖坑法
        • 快慢指针法
      • 2、随机化快速排序
      • 3、三向切分快速排序
      • 4.、插入排序优化
      • 5、非递归优化
      • 总结
    • 七、分而治之,合而有序:深度探索归并排序
      • 1、归并排序的原理
      • 2、归并排序的递归实现
      • 3、归并排序的非递归实现
  • 快乐的时光总是短暂,咱们下篇博文再见啦!!!如果本篇文章对你有帮助的话不要忘了,给小编点点赞和收藏支持一下,在此非常感谢!!!


引言

在浩瀚的数字世界中,比较算法如同一座桥梁,连接着无序与有序,混沌与规律。它们以智慧为笔,以数据为墨,绘制出一幅幅令人叹为观止的排序画卷。

当我们置身于庞大的数据集前,面对杂乱无章的信息海洋,是比较算法赋予了我们一双慧眼,让我们能够迅速洞察数据的内在秩序。通过巧妙的比较与交换,它们将散乱的数据点编织成一条条有序的序列,如同夜空中璀璨的星辰,按照一定的轨迹排列,展现出无尽的魅力。

在本篇博客中,小编将和大家一起踏上探索比较算法的奇妙旅程。从基础的冒泡排序、选择排序到进阶的插入排序、堆排序再到高效的快速排序、归并排序,每一种算法都蕴含着独特的智慧和美感。小编将深入剖析它们的原理,探讨它们的性能特点,感受它们在数据处理中的强大力量。

让我们一起走进比较算法的世界,领略它们的智美风采,共同开启一段充满挑战与收获的旅程吧!

在这里插入图片描述


那接下来就让我们开始遨游在知识的海洋!

正文


一、冒泡排序:数据海洋中的升腾之光

1、定义与原理

  • 冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换的元素为止,也就是说该数列已经排序完成。这个算法的名字由来是因为越小(或越大)的元素会经过交换慢慢“浮”到数列的顶端。
  • 具体来说,冒泡排序从序列中的第一个元素开始,依次对相邻的两个元素进行比较和交换操作。如果前一个元素大于后一个元素,则交换它们的位置;如果前一个元素小于或等于后一个元素,则不进行交换。这一比较和交换的过程一直持续到最后一个还未排好序的元素为止。每一趟操作完成后,序列中最大的未排序元素就被放置到了所有未排序的元素中最后的位置上,而其它较小的元素则被移动到了序列的前面。

2、算法步骤

    1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
    1. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大数。
    1. 针对所有的元素重复以上的步骤,除了最后一个
    1. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

3、性能分析

  • 时间复杂度

    • 最好情况:O(n)(当输入数据已经有序时,只需进行一趟扫描即可确定整个序列已排序,从而立即退出函数)。
    • 平均和最坏情况:O(n^2)。因为冒泡排序需要进行大量的比较和交换操作,且随着数据规模的增大,所需的操作次数呈平方倍增长。
  • 空间复杂度O(1)。冒泡排序只需要一个额外的变量用于交换元素,不需要额外的存储空间来存储中间结果。

  • 稳定性冒泡排序是一种稳定的排序算法。在排序过程中,只有当两个相邻元素的大小关系不满足要求时才进行交换操作。如果两个相等的元素不相邻,即使通过前面的两两交换把它们变成相邻的也不会进行交换操作,因此相等元素的相对位置在排序前后不会改变。


4、优化方案

虽然冒泡排序的时间复杂度较高,但在某些情况下可以通过一些优化方案来提高其效率。例如:

  • 设置一个标志位来检测是否进行了交换操作。如果在某一趟排序中没有进行任何交换操作,则说明整个序列已经有序,可以提前结束排序过程。
  • 记录每一趟排序中最后一次交换操作的位置。下一趟排序时只需要对该位置及其之前的元素进行比较和交换操作即可减少不必要的比较次数。

经典实现:

void Swap(int* p1, int* p2) {
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//冒泡排序
void BubbleSort(int* a, int n) {
	for (int i = 0; i < n; i++) {
		int change = 0;
		for (int j = 0; j < n - i - 1; j++) {
			if (a[j] > a[j + 1]) {
				change = 1;
				Swap(&a[j], &a[j + 1]);
			}
		}
		if (change == 0) break;
	}
}

5、适用场景

  • 冒泡排序适用于小规模数据的排序任务。由于它的时间复杂度较高且缺乏适应性(即使在部分已经有序的情况下仍然需要进行完整的比较和交换操作),因此在处理大规模数据集时效率较低且无法满足实时性要求。然而对于小规模的数据集来说,冒泡排序的实现简单易懂且易于调试和维护,因此在实际应用中仍有一定的使用价值。

综上所述:

  • 冒泡排序作为一种经典的排序算法具有稳定、简单易懂等特点但也存在时间复杂度高、缺乏适应性等缺点

二、选择排序:万千数据中的最优寻觅之旅

1、什么是选择排序

  • 选择排序(Selection Sort)是一种简单直观的排序算法,其基本思想是反复从未排序的数列中选择最小的元素,将其加入到已排序的数列中,最终得到一个有序的数列。这种排序方法既可以从小到大排序,也可以从大到小排序。

2、选择排序的工作原理

选择排序的工作过程如下:

  • 1. 将待排序的数组划分为已排序和未排序两部分。初始时,已排序部分为空,未排序部分为整个数组。
  • 2. 在每一轮排序中,从未排序部分找出最小(或最大)的元素。
  • 3. 将这个最小(或最大)元素与未排序部分的起始位置元素交换,从而将其放入已排序部分的末尾。
  • 4. 重复上述步骤,直到所有元素均排序完毕。

例如,对于数组 [88, 5, 15, 56, 32, 18, 69],按照从小到大的顺序进行排序的过程如下:

  • 第一轮,在未排序部分 [88, 5, 15, 56, 32, 18, 69] 中找到最小的元素 5,将其与未排序部分的起始位置元素 88 交换,得到 [5, 88, 15, 56, 32, 18, 69]
  • 第二轮,在未排序部分 [88, 15, 56, 32, 18, 69] 中找到最小的元素 15,与起始位置元素 88 交换,得到 [5, 15, 88, 56, 32, 18, 69]
  • 依此类推,经过多轮比较和交换,最终使整个数组有序。

3、选择排序的具体步骤

假设要对数组 arr[] 进行排序,数组的长度为 n,具体步骤如下:

    1. 从数组的第一个元素开始,即 i = 0
    1. 对于每个 i,从 i + 1n - 1 中找到最小的元素,并记录其索引 min_index
    1. 如果 min_index 不等于 i,则交换 arr[i]arr[min_index]
    1. 增加 i,重复步骤 2 和 3,直到 i = n - 2

4、C语言实现选择排序

以下是一个用C语言实现选择排序的代码示例:


// 交换两个元素的值
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 选择排序函数
void selectionSort(int arr[], int n) {
    int i, j, min_index;
    for (i = 0; i < n - 1; i++) {
        min_index = i;
        for (j = i + 1; j < n; j++) {
            if (arr[j] < arr[min_index]) {
                min_index = j;
            }
        }
        if (min_index != i) {
            swap(&arr[i], &arr[min_index]);
        }
    }
}

5、选择排序的优缺点

(1)优点
  1. 算法简单:选择排序的原理易懂,实现起来也相对简单。无论是初学者还是经验丰富的程序员,都能快速理解并掌握这种排序方法。
  2. 数据移动次数少:在每次迭代中,只需要移动最小(或最大)的元素到正确的位置,这在一定程度上减少了数据的移动次数。
  3. 空间复杂度低:选择排序法只需要一个额外的变量来保存最小值或最大值的下标,不需要额外的内存空间,是一种原地排序算法。
(2)缺点
  1. 时间复杂度高:选择排序的时间复杂度为 O(n^2),其中 n 为待排序数据的数量。在处理大规模数据时性能不佳。
  2. 不稳定:选择排序是一种不稳定的排序算法。这意味着如果两个元素的值相同,它们在排序后的顺序可能会发生变化。

6、选择排序的应用场景

  1. 小规模数据:由于选择排序的算法复杂度为 O(n^2),它对于小规模数据的排序是非常高效的。
  2. 部分有序数据:如果待排序的数据集合中已经部分有序,即只有少数元素需要进行排序,选择排序是一种合适的选择。
  3. 内存受限环境:选择排序是一种原地排序算法,它只需要一个额外的变量来进行元素交换,这使得它在内存受限的嵌入式系统或其他资源有限的环境中得到广泛应用。

总的来说:

  • 选择排序虽然效率相对较低,但其实现简单直观,非常适合作为学习排序算法的起点。同时,在处理部分有序或规模较小的数据时表现良好,因此在实际应用中也有其独特的价值。

三、堆排序:肩担比较算法里的秩序之责

1、概述

  • 堆排序是一种基于二叉堆数据结构所设计的排序算法,属于选择排序的一种。它通过构建最大堆或最小堆,然后不断删除堆顶元素并调整堆结构来实现排序。堆排序的时间复杂度为O(n log n),空间复杂度为O(1),具有稳定性和适用性广的优点。

2、基本思想

  • 堆是一个近似完全二叉树的结构,同时满足堆积的性质:即子节点的键值总是小于(或者大于)它的父节点。在堆的数据结构中,堆中的最大值(或最小值)总是位于根节点

堆排序的基本思想是:

  • 1. 将待排序的序列构造成一个大顶堆(或小顶堆),此时整个序列的最大值(或最小值)就是堆顶的根节点。
  • 2. 将堆顶元素与末尾元素交换,然后将剩余的堆重新构造成一个堆,得到新的最大值(或最小值)。
  • 3. 重复上述过程,直到堆的大小减至1,此时序列已经完全排序。

3、实现步骤

(1). 构建初始堆
  • 首先需要根据给定的待排序数组构建一个初始堆。构建堆的过程通常是从最后一个非叶子节点开始,向上遍历每个节点,对每个节点进行下沉操作,以确保每个节点都满足堆的性质。

(2). 调整堆并排序

  • 将堆顶元素(最大值或最小值)与末尾元素交换,然后移除末尾元素(或将其视为已排序部分),此时堆的大小减一。接着对剩余部分重新进行下沉操作,以恢复堆的性质。这个过程重复进行,直到堆的大小减至1,排序完成。

4、代码示例

以下是用C语言实现的堆排序代码:

// Function to swap two elements
void swap(int* a, int* b) {
    int t = *a;
    *a = *b;
    *b = t;
}

// Function to heapify a subtree rooted with node i which is an index in arr[]
void heapify(int arr[], int n, int i) {
    int largest = i; // Initialize largest as root
    int left = 2 * i + 1; // left = 2*i + 1
    int right = 2 * i + 2; // right = 2*i + 2

    // See if left child of root exists and is greater than root
    if (left < n && arr[left] > arr[largest])
        largest = left;

    // See if right child of root exists and is greater than root
    if (right < n && arr[right] > arr[largest])
        largest = right;

    // Change root, if needed
    if (largest != i) {
        swap(&arr[i], &arr[largest]);

        // Heapify the root.
        heapify(arr, n, largest);
    }
}

// Main function to do heap sort
void heapSort(int arr[], int n) {
    // Build heap (rearrange array)
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);

    // One by one extract an element from heap
    for (int i = n - 1; i >= 0; i--) {
        // Move current root to end
        swap(&arr[0], &arr[i]);

        // call max heapify on the reduced heap
        heapify(arr, i, 0);
    }
}

5、优缺点分析

(1)优点

1.时间复杂度稳定 :无论输入数组的状态如何,堆排序的时间复杂度总是O(n log n)。
2. 空间复杂度低 :堆排序是原地排序算法,只需要常数个额外的空间,空间复杂度为O(1)。

(2)缺点
  1. 不稳定排序 :堆排序是不稳定排序,相同元素的相对顺序可能会被改变。
  2. 常数系数较大 :尽管堆排序的时间复杂度和快速排序相同,但堆排序的常数系数较大,实际运行速度往往比不上快速排序。

6、总结

  • 堆排序是一种高效且稳定的排序算法,特别适用于处理大型数据集和外部排序场景。虽然它在某些方面可能不如其他排序算法出色,但在许多实际应用中仍然是一种非常有用的工具。

四、插入排序:简单却强大的数据整理工具

1、概述

  • 插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上通常使用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,找到合适位置并插入时,不需要移动其它元素。

2、算法步骤

  1. 初始状态:假设待排序数组为A[0...n-1],其中前i-1个元素已经排好序(即A[0...i-1]是有序的),第i个元素到最后一个元素A[i...n-1]是无序的。
  2. 取无序区首元素:取出无序区的第一个元素A[i],将其视为“当前元素”。
  3. 在有序区寻找插入位置:将当前元素与有序区的元素进行比较,如果当前元素比有序区的某个元素小,则将该有序区的元素向后移动一位,直到找到当前元素的正确插入位置。
  4. 插入当前元素:将当前元素插入到找到的位置上,此时有序区长度增加1,无序区长度减少1。
  5. 重复上述过程:对i=1, 2, ..., n-1均执行上述操作,直至整个数组有序。

3、示例

假设有一个数组[5, 2, 9, 1, 5, 6],我们对其进行插入排序:

  • 1. 初始状态:[5, 2, 9, 1, 5, 6]
  • 2. i=1A[1]=2,小于A[0]=5,将5右移,2插入到首位:[2, 5, 9, 1, 5, 6]
  • 3. i=2A[2]=9,大于A[1]=5,无需移动,保持原样:[2, 5, 9, 1, 5, 6]
  • 4. i=3A[3]=1,小于A[2]=9A[1]=5以及A[0]=2,依次将952右移,1插入到首位:[1,2, 5, 9, 5, 6]
  • 5. i=4A[4]=5,小于A[3]=9但大于A[2]=5A[1]=2以及A[0]=1,将9右移,5插入到第三位:[1, 2, 5, 5, 9, 6]
  • 6. i=5A[5]=6,小于A[4]=9但大于前面的所有元素,将9右移,6插入到最后一位:[1, 2, 5, 5, 6, 9]
  • 最终得到有序数组[1, 2, 5, 5, 6, 9]

代码为:

//插入排序-------O(N ^ 2)
void InsertSort(int* a, int n) {
	for (int i = 1; i < n; i++) {
		int end = i - 1;					//对应有序数字最后一个元素的下标
		int temp = a[end + 1];					//end的后一个,也就是要插入的元素
		while (end >= 0) {
			if (temp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else {
				break;
			}
		}
		a[end + 1] = temp;
	}
}


4、时间复杂度分析

  • 最坏情况:当输入数组是逆序的时,需要进行n(n-1)/2次比较和交换操作,因此时间复杂度为O(n^2)。
  • 最好情况:当输入数组已经是有序的时,只需要进行n-1次比较而无需交换操作,因此时间复杂度为O(n)。
  • 平均情况:时间复杂度仍然为O(n^2),因为即使输入数组部分有序,也可能需要多次比较和交换操作来维护有序性。
  • 尽管插入排序在最坏情况下的时间复杂度较高,但由于其实现简单且在小规模数据集上表现良好,因此在某些情况下仍具有实用价值。

五、希尔之光:跨越间隔的优雅排序之旅

1、引言

  • 在计算机科学中,排序算法是数据处理领域的基础和核心。在众多排序算法中,希尔排序(Shell Sort)以其独特的分治策略和高效的性能脱颖而出,成为众多开发者青睐的选择。本文将详细介绍希尔排序的原理、实现步骤以及它的优缺点,带你领略这一算法的优雅与魅力。

2、希尔排序的原理

  • 希尔排序是由计算机科学家Donald Shell于1959年提出的一种基于插入排序的改进算法。它通过将待排序数组分割成若干个子序列,分别对每个子序列进行插入排序,从而逐步减少数据的无序程度,最终实现对整个数组的排序。
  • 希尔排序的关键在于选择合适的“间隔(gap)”序列。初始时,选择一个较大的间隔值,将数组分割成多个子序列;然后,对每个子序列进行插入排序;接着,逐渐减小间隔值,重复上述过程,直到间隔值为1时,对整个数组进行一次最终的插入排序。这样,通过多次局部有序化,可以大大加快整体排序的速度。

3、希尔排序的实现步骤

  • 初始化间隔值:选择一个合适的初始间隔值,通常可以选择数组长度的一半或更小的值作为起始间隔。
  • 分组排序:根据当前间隔值,将数组分割成多个子序列,对每个子序列进行插入排序。
  • 更新间隔值:按照一定的规则(如减半)更新间隔值,继续对新的子序列进行插入排序。
  • 重复步骤2和3:直到间隔值为1时,对整个数组进行一次最终的插入排序。

以下是一个简单的希尔排序C语言实现示例:

 
//希尔排序-------O(N ^ 1.3)
void ShellSort(int* a, int n) {
	int gap = n;
	while (gap > 1) {
		gap /= 2;
		/*for (int i = 0; i < gap; i++) {
			for (int j = i; j < n - gap; j += gap) {
				int end = j;
				int tmp = a[end + gap];
				while (end >= 0) {
					if (tmp < a[end]) {
						a[end + gap] = a[end];
						end -= gap;
					}
					else {
						break;
					}
				}
				a[end + gap] = tmp;
			}
		}*/

		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;
				}
			}
			a[end + gap] = tmp;
		}
		PrintArray(a, n);
		printf("\n");
	}
}

4、希尔排序的优缺点

(1)优点
  • 效率高:相比于简单的插入排序,希尔排序通过多次局部有序化,减少了数据移动的次数,提高了排序效率。
  • 稳定性好:虽然希尔排序不是稳定的排序算法(即相同元素的相对顺序可能会改变),但在实际应用中,其稳定性问题通常可以忽略不计。
  • 适用范围广:希尔排序适用于各种类型的数据,包括整数、浮点数和字符串等。
(2)缺点
  • 间隔值选择困难:希尔排序的性能很大程度上取决于间隔值的选择。不同的间隔值序列可能导致截然不同的排序效果。因此,如何选择合适的间隔值序列是一个难题。
  • 最坏情况性能不佳:在某些情况下,希尔排序的最坏时间复杂度可能达到O(n^2),尽管在实际应用中这种情况较为罕见。

四、总结与展望

  • 希尔排序作为一种经典的排序算法,以其独特的分治策略和高效的性能在数据处理领域发挥着重要作用。然而,随着计算机科学的发展和新算法的涌现,希尔排序也面临着来自其他更高效排序算法的挑战。未来,我们可以期待更多关于希尔排序的优化和改进,以应对更加复杂和多样化的数据处理需求。同时,我们也可以借鉴希尔排序的思想和方法,探索和发展更多新的排序算法和技术。

六、快速排序:速度与效率的完美平衡

快速排序是一种非常高效的排序算法,其基本思想是通过一个划分操作将待排序的数组分为两个子数组,左边子数组的元素都比右边子数组的元素小,然后递归地对这两个子数组进行排序。下面是快速排序的多种版本及其实现原理和优化方法的详细介绍。

1. 基本快速排序

原理

  • 选择基准:从数组中选择一个元素作为基准(pivot)。
  • 分区操作:将数组分为两部分,左边部分的所有元素都小于基准,右边部分的所有元素都大于基准。
  • 递归排序:对左右两个子数组递归地进行快速排序。

三种版本:

霍尔法(Hoare法)

霍尔法是以快速排序的创始人霍尔命名的,也是快速排序最初的实现版本。其基本思想是用两个指针分别指向待排序数组的开头和结尾,然后让这两个指针相向而行,根据与key值的大小关系进行移动和交换,直到两者相遇。具体步骤如下:

任取一个待排序数组中的数作为key值(可以取头值、尾值或中间值等)。

如果取头值作为key值,则让右指针先移动;如果取尾值作为key值,则让左指针先移动。“移动”的过程是:

right指针直到找到小于key的值才停下来,left指针直到找到大于key的值才停下来。

将left和right所对应的值进行交换。

重复上述过程,直到left和right相遇。此时,将key值和right所指向的值进行交换,right最后指向的位置就是key值应该所在的位置。

以right为界,将数组一分为二,递归地对左右两部分进行排序。

代码实现:

//快速排序-----霍尔
void QuickSort1(int* a, int left, int right) {
	if (left >= right) return;
	int end = right, begin = left, keyi = left;
	while (left < right) {
		//右指针找小
		while (left < right && a[right] >= a[keyi]) {
			--right;
		}
		
		//左指针找大
		while (left < right && a[left] <= a[keyi]) {
			++left;
		}

		Swap(&a[right], &a[left]);
	}
	//最后对调将目标值放到合适的位置
	Swap(&a[keyi], &a[left]);
	keyi = left;

	QuickSort1(a, begin, keyi - 1);
	QuickSort1(a, keyi + 1, end);
}

霍尔法的优势在于其高效性,但理解起来可能相对复杂一些。


挖坑法

挖坑法是快速排序的一种实现方式,其思路与霍尔法类似,但更容易理解。具体步骤如下:

选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑。
定义left和right两个指针,分别从数组的左右两端开始相向而行。
right指针向左移动,直到找到比key小的数;然后将该数填入坑中,并将hole更新为当前right的位置。
left指针向右移动,直到找到比key大的数;然后将该数填入当前的hole中,并再次更新hole为当前left的位置。
重复上述步骤,直到left和right相遇。此时,将key值填入最后的hole中。
以hole为界,将数组一分为二,递归地对左右两部分进行排序。

代码实现:

//快速排序-----挖坑
void QuickSort2(int* a, int left, int right) {
	if (left >= right) return;
	int end = right, begin = left;
	int midi = GetMidNumi(a, left, right);
	Swap(&a[left], &a[midi]);
	int key = a[left], 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;

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

挖坑法的优势在于其直观易懂,便于理解和实现。


快慢指针法

这种方法使用两个指针从数组的两端向中间移动,直到它们相遇或交错。

代码实现:

//快速排序-----快慢指针
void QuickSort3(int* a, int left, int right) {
	if (left >= right) return;
	int keyi = left;
	int prev = left, cur = left + 1;
	int midi = GetMidNumi(a, left, right);
	Swap(&a[left], &a[midi]);
	while (cur <= right){
		if (a[cur] < a[keyi]) {
			Swap(&a[cur], &a[++prev]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	QuickSort3(a, left, keyi - 1);
	QuickSort3(a, keyi + 1, right);
}

2、随机化快速排序

原理

  • 随机选择基准:在每次划分之前,随机选择一个元素作为基准,以减少最坏情况的发生概率。
  • 分区操作:与基本快速排序相同。
  • 递归排序:对左右两个子数组递归地进行快速排序。

代码示例

#include <stdlib.h>

int randomPartition(int arr[], int low, int high) {
    int random = low + rand() % (high - low + 1);
    swap(&arr[random], &arr[high]);
    return partition(arr, low, high);
}

void randomizedQuickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = randomPartition(arr, low, high);

        randomizedQuickSort(arr, low, pi - 1);
        randomizedQuickSort(arr, pi + 1, high);
    }
}

3、三向切分快速排序

原理

  • 选择基准:选择一个元素作为基准。
  • 三向分区:将数组分为三部分:
  • 左边部分的所有元素都小于基准。
  • 中间部分的所有元素都等于基准。
  • 右边部分的所有元素都大于基准。
  • 递归排序:只对左右两个子数组递归地进行快速排序,中间部分已经有序。

代码示例

void threeWayQuickSort(int arr[], int low, int high) {
    if (high <= low) return;

    int lt = low, gt = high;
    int pivot = arr[low];
    int i = low + 1;

    while (i <= gt) {
        if (arr[i] < pivot) {
            swap(&arr[lt++], &arr[i++]);
        } else if (arr[i] > pivot) {
            swap(&arr[i], &arr[gt--]);
        } else {
            i++;
        }
    }

    threeWayQuickSort(arr, low, lt - 1);
    threeWayQuickSort(arr, gt + 1, high);
}

4.、插入排序优化

原理

  • 选择基准:选择一个元素作为基准。
  • 分区操作:将数组分为两部分。
  • 递归排序:当子数组的大小小于某个阈值时,使用插入排序而不是快速排序,以减少递归调用的开销。

代码示例

void hybridQuickSort(int arr[], int low, int high, int threshold) {
    while (low < high) {
        if (high - low < threshold) {
            insertionSort(arr, low, high);
            break;
        } else {
            int pi = partition(arr, low, high);

            hybridQuickSort(arr, low, pi - 1, threshold);
            low = pi + 1;
        }
    }
}

void insertionSort(int arr[], int low, int high) {
    for (int i = low + 1; i <= high; i++) {
        int key = arr[i];
        int j = i - 1;

        while (j >= low && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}
 

5、非递归优化

因为递归算法都存在一个无法避免的问题——递归深度太大会导致栈溢出的问题,所以我们应该掌握非递归实现各种递归算法的技能。

  • 递归算法改非递归一般有两种思路:(1)直接改循环;(2)借用数据结构其中栈是最为常用的
  • 对于快速排序,使用栈是最好模拟的方式。

代码实现:

(1)ST.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

#define MAXCAPACITY 4

typedef int Datastyle;

typedef struct stack {
	Datastyle* a;               //指向有效数据的指针 
	int top;
	//给top初始化一般要么给-1,要么给0,如果给1及以上就会造成空间的浪费,给-2及以下也会造成空间的浪费(除非进行特殊的处理,分情况给top加值)
	//如果top初始化给的是-1,则top代表的是栈顶元素所在位置的下标;如果top初始化给的是0,则top代表的是栈顶元素所在位置的下一个位置的下标
	//这是因为(以插入为例)每一次入栈后,top必定加1-----而top初始化给-1元素入栈就是top先++再赋值;而top初始化给0元素入栈就是top先赋值再++
	//但无论哪种入栈方式,最终的结果都是top++。至于为什么两种不同的给top赋初值对应不同的入栈方式也是取决于数组的下标从0开始的特点
	//本次我们就展示top初始化为0的栈的写法
	int capacity;
}ST;

//初始化栈
void STInit(ST* ps);

//销毁栈
void  STDestory(ST* ps);

//插入数据(从栈顶)----(入栈,压栈)
void STPush(ST* ps, Datastyle x);

//删除数据(从栈顶)----(出栈)
void STPop(ST* ps);

//访问栈顶元素
Datastyle STTop(ST* ps);

//得出栈的元素个数
int STSize(ST* ps);

//判空
bool STEmpty(ST* ps);

ST.c

#include"ST.h"

//初始化栈
void STInit(ST* ps) {
	assert(ps);
	Datastyle* temp = (Datastyle*)malloc(MAXCAPACITY * sizeof(Datastyle));
	if (temp == NULL) {
		perror("malloc fail");
		exit(-1);
	}
	ps->a = temp;
	ps->capacity = MAXCAPACITY;
	ps->top = 0;
}

//销毁栈
void  STDestory(ST* ps){
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = 0;
	ps->top = 0;
}

//插入数据(从栈顶)----(入栈,压栈)
void STPush(ST* ps, Datastyle x) {
	assert(ps);
	//判断是否满了
	if (ps->top == ps->capacity) {
		Datastyle* temp = (Datastyle*)realloc(ps->a, 2 * ps->capacity * sizeof(Datastyle));				//扩容为当前容量的两倍比较合理
		if (temp == NULL) {
			perror("realloc fail");
			return;
		}
		ps->capacity *= 2;
		ps->a = temp;
	}
	ps->a[ps->top++] = x;
}

//判空
bool STEmpty(ST* ps) {
	return (ps->top == 0);
}

//删除数据(从栈顶)----(出栈)
void STPop(ST* ps) {
	assert(ps && !STEmpty(ps));
	--ps->top;
}

//访问栈顶元素
Datastyle STTop(ST* ps) {
	return ps->a[ps->top - 1];
}

//得出栈的元素个数
int STSize(ST* ps) {
	assert(ps);
	return ps->top;
}



Sort.c

#include"ST.h"
//快速排序-----非递归实现------入栈元素:每次递归会发生改变的参数
void QuickSortNonR(int* a, int left, int right) {
	//Chaucer创建栈
	ST st;

	//初始化栈
	STInit(&st);

	//先入栈
	STPush(&st, right);
	STPush(&st, left);

	while (!STEmpty(&st)){
		//因为栈先入后出的特点,所以我们先入右再入左
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);

		//进行单趟排序并返回keyi
		int keyi = SingalQuickSort(a, begin, end);
		//[begin,keyi - 1] keyi [keyi + 1, end] 
		
		//因为栈先入后出的特点,所以我们先入右再入左,不过当区间内仅剩一个元素或没有元素的时候,我们也不应该入栈
		if (keyi + 1 < end) {
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1) {
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}

	//销毁栈
	STDestory(&st);
}


总结

  • 快速排序的多种版本和优化方法可以显著提高其在不同场景下的性能。选择合适的版本和优化方法,可以根据具体的数据特性和应用场景,进一步提升排序算法的效率和稳定性。希望这些介绍能帮助你更好地理解和应用快速排序算法。

七、分而治之,合而有序:深度探索归并排序

1、归并排序的原理

归并排序的基本思想是将一个待排序的数组分成两个小数组,分别对这两个小数组进行排序,然后将这两个已排序的小数组合并成一个最终的已排序数组。其关键步骤包括分解和合并两个阶段:

  • 分解阶段:将待排序的数组分割成两个子数组,直到每个子数组的长度小于或等于1(此时认为是有序的)。
  • 合并阶段:将两个有序的子数组合并成一个更大的有序数组,直到最终合并为整个数组的排序结果。
  • 归并排序的时间复杂度为O(n log n),其中n是待排序数组的元素个数。这是因为每次分解都将数组规模减半,而合并操作则需要遍历整个数组。由于分解和合并都是线性时间复杂度的操作,因此总的时间复杂度为O(n log n)。
  • 此外,归并排序是一种稳定的排序算法,即相同元素的相对顺序在排序前后保持不变。

2、归并排序的递归实现

  • 递归实现是归并排序的一种常见方式。其基本思路是不断地将数组分解成更小的子数组,直到每个子数组只有一个元素或为空,然后再将这些子数组合并起来形成有序的数组。

以下是归并排序递归实现的C语言代码示例:

#include <stdio.h>
#include <stdlib.h>

// 合并两个有序数组arr[left...mid]和arr[mid+1...right]到temp[]中
void merge(int arr[], int left, int mid, int right, int temp[]) {
   int i = left;   // 左子数组的起始索引
   int j = mid + 1;// 右子数组的起始索引
   int k = 0;      // 临时数组的索引

   // 将较小的元素放入临时数组中
   while (i <= mid && j <= right) {
       if (arr[i] <= arr[j]) {
           temp[k++] = arr[i++];
       } else {
           temp[k++] = arr[j++];
       }
   }

   // 将剩余的元素放入临时数组中
   while (i <= mid) {
       temp[k++] = arr[i++];
   }
   while (j <= right) {
       temp[k++] = arr[j++];
   }

   // 将临时数组中的元素复制回原数组中
   for (int p = 0; p < k; p++) {
       arr[left + p] = temp[p];
   }
}

// 使用递归对数组arr[left...right]进行归并排序
void mergeSort(int arr[], int left, int right, int temp[]) {
   if (left < right) {
       int mid = left + (right - left) / 2;

       // 对左半部分进行排序
       mergeSort(arr, left, mid, temp);

       // 对右半部分进行排序
       mergeSort(arr, mid + 1, right, temp);

       // 合并左右两部分
       merge(arr, left, mid, right, temp);
   }
}

// 归并排序的主函数
void MergeSortWrapper(int arr[], int n) {
   int *temp = (int *)malloc(n * sizeof(int));
   if (temp == NULL) {
       fprintf(stderr, "Memory allocation failed
");
       exit(EXIT_FAILURE);
   }
   mergeSort(arr, 0, n - 1, temp);
   free(temp);
}

int main() {
   int arr[] = {38, 27, 43, 3, 9, 82, 10};
   int n = sizeof(arr) / sizeof(arr[0]);

   printf("Given array is 
");
   for (int i = 0; i < n; i++) {
       printf("%d ", arr[i]);
   }
   printf("
");

   MergeSortWrapper(arr, n);

   printf("Sorted array is 
");
   for (int i = 0; i < n; i++) {
       printf("%d ", arr[i]);
   }
   printf("
");

   return 0;
}

  • 在这个例子中,merge 函数负责将两个有序的子数组合并成一个有序的数组,而 mergeSort 函数则使用递归来对数组进行分解和排序。最后, MergeSortWrapper 函数为 mergeSort 分配了一个临时数组,并在排序完成后释放了它。

3、归并排序的非递归实现

  • 虽然递归实现简洁明了,但在某些情况下可能会导致栈溢出或效率问题。因此,非递归实现也是一种重要的选择。

非递归实现通常采用迭代的方式来进行归并操作。以下是非递归实现的C语言代码示例:

 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 非递归归并排序函数
void mergeSortNonRecursive(int arr[], int n) {
    int *temp = (int *)malloc(n * sizeof(int));
    if (temp == NULL) {
        fprintf(stderr, "Memory allocation failed
");
        exit(EXIT_FAILURE);
    }

    int gap = 1; // 每次归并操作的子数组大小
    while (gap < n) {
        // 进行一轮归并操作
        for (int i = 0; i < n; i += 2 * gap) {
            int begin1 = i;
            int end1 = i + gap - 1;
            int begin2 = i + gap;
            int end2 = i + 2 * gap - 1;

            // 处理边界情况
            if (end1 >= n) {
                end1 = n - 1;
            }
            if (begin2 >= n) {
                break; // 没有第二个子数组需要归并了
            }
            if (end2 >= n) {
                end2 = n - 1;
            }

            // 合并两个子数组
            int j = begin1;
            int k = 0;
            while (begin1 <= end1 && begin2 <= end2) {
                if (arr[begin1] <= arr[begin2]) {
                    temp[k++] = arr[begin1++];
                } else {
                    temp[k++] = arr[begin2++];
                }
            }
            while (begin1 <= end1) {
                temp[k++] = arr[begin1++];
            }
            while (begin2 <= end2) {
                temp[k++] = arr[begin2++];
            }

            // 将合并后的数组复制回原数组
            memcpy(arr + i, temp + begin1 - i, (end2 - i + 1) * sizeof(int));
        }

        // 增加gap的值以便下一轮归并操作
        gap *= 2;
    }

    free(temp);
}

int main() {
    int arr[] = {38, 27, 43, 3, 9, 82, 10};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("Given array is 
");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("
");

    mergeSortNonRecursive(arr, n);

    printf("Sorted array is 
");
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("
");

    return 0;
}

快乐的时光总是短暂,咱们下篇博文再见啦!!!如果本篇文章对你有帮助的话不要忘了,给小编点点赞和收藏支持一下,在此非常感谢!!!

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

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

相关文章

mysql-5.7.18保姆级详细安装教程

本文主要讲解如何安装mysql-5.7.18数据库&#xff1a; 将绿色版安装包mysql-5.7.18-winx64解压后目录中内容如下图&#xff0c;该例是安装在D盘根目录。 在mysql安装目录中新建my.ini文件&#xff0c;文件内容及各配置项内容如下图&#xff0c;需要先将配置项【skip-grant-tab…

2025年华数杯国际赛B题论文首发+代码开源 数据分享+代码运行教学

176项指标数据库 任意组合 千种组合方式 14页纯图 无水印可视化 63页无附录正文 3万字 1、为了方便大家阅读&#xff0c;全文使用中文进行描述&#xff0c;最终版本需自行翻译为英文。 2、文中图形、结论文字描述均为ai写作&#xff0c;可自行将自己的结果发给ai&#xff0c…

unity学习17:unity里的旋转学习,欧拉角,四元数等

目录 1 三维空间里的旋转与欧拉角&#xff0c;四元数 1.1 欧拉角比较符合直观 1.2 四元数 1.3 下面是欧拉角和四元数的一些参考文章 2 关于旋转的这些知识点 2.1 使用euler欧拉角旋转 2.2 使用quaternion四元数,w,x,y,z 2.3 使用quaternion四元数,类 Vector3.zero 这种…

深度剖析RabbitMQ:从基础组件到管理页面详解

文章目录 一、简介二、Overview2.1 Overview->Totals2.2 Overview->Nodesbroker的属性2.3 Overview->Churn statistics2.4 Overview->Ports and contexts2.5 Overview->Export definitions2.6 Overview->Import definitions 三、Connections连接的属性 四、C…

机器学习中的凸函数和梯度下降法

一、凸函数 在机器学习中&#xff0c;凸函数 和 凸优化 是优化问题中的重要概念&#xff0c;许多机器学习算法的目标是优化一个凸函数。这些概念的核心思想围绕着优化问题的简化和求解效率。下面从简单直观的角度来解释。 1. 什么是凸函数&#xff1f; 数学定义 一个函数 f…

使用 WPF 和 C# 绘制覆盖网格的 3D 表面

此示例展示了如何使用 C# 代码和 XAML 绘制覆盖有网格的 3D 表面。示例使用 WPF 和 C# 将纹理应用于三角形展示了如何将纹理应用于三角形。此示例只是使用该技术将包含大网格的位图应用于表面。 在类级别&#xff0c;程序使用以下代码来定义将点的 X 和 Z 坐标映射到 0.0 - 1.…

深入Android架构(从线程到AIDL)_32 JNI架构原理_Java与C的对接05

1、EIT造形观点 基于熟悉的EIT造形&#xff0c;很容易理解重要的架构设计决策议题。 前言 2、混合式EIT造形 一般EIT造形是同语言的。也就是<E>、 <I>和<T>都使用同一种语言撰写的&#xff0c;例如上述的Java、 C/C等。于此&#xff0c;将介绍一个EIT造…

数字普惠金融对新质生产力的影响研究(2015-2023年)

基于2015—2023年中国制造业上市公司数据&#xff0c;探讨了数字普惠金融对制造业企业新质生产力的影响及作用机理。研究发现&#xff0c;数字普惠金融有助于促进制造业企业新质生产力的发展&#xff0c;尤其是在数字普惠金融的使用深度较大的情况下&#xff0c;其对新质生产力…

装备制造业:建立项目“四算”管理:以合同为源头,以项目为手段实现合同的测算、预算、核算与决算的管控体系

尊敬的各位管理层&#xff1a; 大家好&#xff01;作为装备制造业的 CFO&#xff0c;我今天要向大家汇报的是如何建立项目“四算”管理&#xff0c;即以合同为源头&#xff0c;以项目为手段实现合同的测算、预算、核算与决算的管控体系。在当前市场竞争激烈、成本压力不断增大…

自建RustDesk服务器

RustDesk服务端 下面的截图是我本地的一个服务器做为演示用&#xff0c;你自行的搭建服务需要该服务器有固定的ip地址 1、通过宝塔面板快速安装 2、点击【安装】后会有一个配置信息&#xff0c;默认即可 3、点击【确认】后会自动安装等待安装完成 4、安装完成后点击【打开…

前端实现doc文件预览的三种方式

文章目录 1、docx-preview 实现&#xff08;推荐&#xff09;2、vue-office 实现3、mammoth 实现&#xff08;不推荐&#xff09; 需求&#xff1a;有一个docx文件&#xff0c;需要按其本身的格式&#xff0c;将内容展示出来&#xff0c;即&#xff1a;实现doc文件预览。 本文…

final修饰的用法

1、final修饰类 被final修饰的类不可以在被继承。 比如在Java中String就是final修饰的不可以被继承 2、final修饰成员变量 同时final也可以修饰局部变量 final int N5; 3、final修饰静态变量 final修饰静态的成员变量&#xff0c;&#xff08;在方法中不能定义静态的属性…

Windows 11 安装GTK+3.0 和VScode开发GTK+3.0配置

Windows 11 安装GTK+3.0 和VScode开发GTK+3.0配置 安装msys2下载msys2安装安装msys2安装编译器gcc安装调试器gdb安装GTK+3.0安装C/C++开发GTK+3.0工具配置路径验证GTK+3.0安装验证配置运行GTK DemoVScode配置测试代码文件test.c任务配置文件tasks.jsongdb调试配置文件launch.js…

鸿蒙-页面和自定义组件生命周期

页面生命周期&#xff0c;即被Entry装饰的组件生命周期&#xff0c;提供以下生命周期接口&#xff1a; onPageShow&#xff1a;页面每次显示时触发一次&#xff0c;包括路由过程、应用进入前台等场景。onPageHide&#xff1a;页面每次隐藏时触发一次&#xff0c;包括路由过程、…

国产编辑器EverEdit - 扩展脚本:新建同类型文件(避免编程学习者反复新建保存练习文件)

1 扩展脚本&#xff1a;在当前文件目录下新建同类型文件 1.1 应用场景 用户在进行编程语言学习时&#xff0c;比如&#xff1a;Python&#xff0c;经常做完一个小练习后&#xff0c;又需要新建一个文件&#xff0c;在新建文件的时候&#xff0c;不但要选择文件类型&#xff0c…

使用 selenium-webdriver 开发 Web 自动 UI 测试程序

优缺点 优点 有时候有可能一个改动导致其他的地方的功能失去效果&#xff0c;这样使用 Web 自动 UI 测试程序可以快速的检查并定位问题&#xff0c;节省大量的人工验证时间 缺点 增加了维护成本&#xff0c;如果功能更新过快或者技术更新过快&#xff0c;维护成本也会随之提高…

算法-贪心算法简单介绍

下面是贪心算法视频课的导学内容. 目录 1. 什么是贪心算法?2. 贪心算法简单的三个例子:1. 找零问题2. 最小路径和问题3. 背包问题 3. 贪心算法的特点4. 贪心算法学习的方式? 1. 什么是贪心算法? 简单来说, 我们称以局部最优进而使得全局最优的一种思想实现出来的算法为贪心…

【Hive】新增字段(column)后,旧分区无法更新数据问题

TOC 【一】问题描述 Hive修改数据表结构的需求&#xff0c;比如&#xff1a;增加一个新字段。 如果使用如下语句新增列&#xff0c;可以成功添加列col1。但如果数据表tb已经有旧的分区&#xff08;例如&#xff1a;dt20190101&#xff09;&#xff0c;则该旧分区中的col1将为…

《深度剖析算法优化:提升效率与精度的秘诀》

想象一下&#xff0c;你面前有一堆杂乱无章的数据&#xff0c;你需要从中找到特定的信息&#xff0c;或者按照一定的规则对这些数据进行排序。又或者&#xff0c;你要为一个物流公司规划最佳的配送路线&#xff0c;以降低成本和提高效率。这些问题看似复杂&#xff0c;但都可以…

量子计算:从薛定谔的猫到你的生活

文章背景 说到量子计算&#xff0c;不少人觉得它神秘又遥不可及。其实&#xff0c;它只是量子物理学的一个“应用小分支”。它的核心在于量子比特的“叠加”和“纠缠”&#xff0c;这些听上去像科幻小说的概念&#xff0c;却为计算世界开辟了一片全新的天地。如果经典计算是“…