数据结构:交换排序的实现

概要

        交换排序是一类通过比较和交换元素位置来实现排序的算法。其核心思想是在序列中进行两两比较,若元素顺序不符合排序要求,则交换它们的位置。常见的交换排序算法包括冒泡排序和快速排序,它们在不同场景下各有优劣。

整体架构流程

冒泡排序

  1. 从数组的第一个元素开始,依次比较相邻的两个元素;
  2. 如果前一个元素大于后一个元素(假设为升序排序),则交换这两个元素的位置;
  3. 对数组中的每一对相邻元素都执行上述操作,经过一轮比较后,最大的元素会 “浮” 到数组的末尾;
  4. 重复上述步骤,每次比较的范围减少一个元素(因为上一轮已经将最大元素排到了末尾),直到整个数组有序。

快速排序

  1. 选择一个基准元素(pivot),通常选择数组的第一个元素或中间元素;
  2. 重新排列数组,将所有比基准元素小的元素移到基准元素的左边,所有比基准元素大的元素移到基准元素的右边。这个过程称为分区(partition)操作;
  3. 递归地对基准元素左边和右边的子数组进行快速排序,直到子数组的长度为 1 或 0(此时子数组已经有序)。

技术名词解释

冒泡排序(Bubble Sort)

        一种简单的交换排序算法,通过多次比较相邻元素并交换位置,使较大(或较小)的元素逐渐 “冒泡” 到数组的末尾(或开头)。其时间复杂度在最坏和平均情况下为 O (n²),空间复杂度为 O (1),适用于小规模数据的排序。

快速排序(Quick Sort)

        高效的交换排序算法,采用分治思想。通过选择基准元素并进行分区操作,将数组分为两部分,分别对两部分递归排序。平均时间复杂度为 O (n log n),最坏时间复杂度为 O (n²)(当基准元素选择不当,如数组已经有序时),空间复杂度在平均情况下为 O (log n),适用于大规模数据的排序。

分区(Partition)

        快速排序中的关键操作。将数组按照基准元素分为两部分,左边部分的元素都小于基准元素,右边部分的元素都大于基准元素。这个操作确定了基准元素在最终有序数组中的正确位置,并且使得左边和右边的子数组都更接近有序状态,为后续递归排序做好准备。

技术细节

冒泡排序实现细节

#include 

void bubbleSort(int arr[], int sz) {
	int i = 0;
	int j = 0;
	int temp = 0; // 第三方存储器
	int flag = 1; // 用于查看是否不需要排序,1 --- 不需要排序,0 --- 需要排序

	// 遍历数组多趟
	for (i = 0; i < sz - 1; i++) {
		// 每一趟的操作
		flag = 1; // 每趟都需要重置

		for (j = 0; j < sz - 1 - i; j++) {
			if (arr[j] > arr[j + 1]) {
				flag = 0;
				temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}

		// 如果不需要排序,则直接结束函数
		if (flag) {
			return;
		}
	}
}

int main() {
	int arr[] = { 1, 5, 2, 12, 54, 2, 23 };
	int sz = sizeof(arr) / sizeof(arr[0]); // 数组大小

	bubbleSort(arr, sz); // 冒泡排序

	// 查看排序结果
	int i = 0;
	for (i = 0; i < sz; i++) {
		printf("%d ", arr[i]);
	}

	return 0;
}

        在这段代码中,外层循环控制排序的轮数,每一轮会将一个最大元素移到数组末尾。内层循环负责在每一轮中进行相邻元素的比较和交换,比较的范围随着轮数增加而逐渐减少(sz - 1 - i)。

        同时我还引入了一个flag标志位,用于优化排序过程。在每一轮开始时,将flag置为 1,如果在某一轮的比较中发生了元素交换,就将flag置为 0。如果某一轮结束后flag仍为 1,说明在这一轮中没有元素交换,数组已经有序,此时可以提前结束排序,从而提高排序效率。

快速排序实现细节

#include 

// 分区函数:将数组分为两部分,返回基准值的位置
int partition(int arr[], int left, int right) {
    int pivotKey = arr[left]; // 选择第一个元素作为基准值

    while (left < right) { // 当左指针小于右指针时循环
        // 从右向左扫描,找到第一个小于基准值的元素
        while (left < right && arr[right] >= pivotKey) {
            right--; // 右指针左移
        }
        arr[left] = arr[right]; // 将小于基准值的元素放到左边

        // 从左向右扫描,找到第一个大于基准值的元素
        while (left < right && arr[left] <= pivotKey) {
            left++; // 左指针右移
        }
        arr[right] = arr[left]; // 将大于基准值的元素放到右边
    }

    arr[left] = pivotKey; // 将基准值放到正确的位置
    return left; // 返回基准值的位置
}

// 快速排序函数
void qSort(int arr[], int left, int right) {
    // 如果左指针小于右指针,继续排序
    if (left < right) {
        int pivotLoc = partition(arr, left, right); // 获取基准值的位置

        // 递归排序左半部分
        qSort(arr, left, pivotLoc - 1);
        // 递归排序右半部分
        qSort(arr, pivotLoc + 1, right);
    }
}

int main() {
    int arr[] = { 1, 5, 2, 12, 54, 2, 23 }; // 待排序的数组
    int sz = sizeof(arr) / sizeof(arr[0]); // 计算数组的大小

    qSort(arr, 0, sz - 1); // 调用快速排序函数

    // 输出排序后的数组
    printf("排序后的数组: ");
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0; // 程序正常结束
}

        在这段代码中,partition 函数负责实现分区操作。首先选择数组的第一个元素 arr[left] 作为基准值 pivotKey。然后通过两个嵌套的 while 循环,从数组的两端向中间扫描。第一个内层 while 循环从右向左扫描,找到第一个小于基准值的元素,将其移动到左边;第二个内层 while 循环从左向右扫描,找到第一个大于基准值的元素,将其移动到右边。

        这样不断交换,直到左指针 left 和右指针 right 相遇,此时将基准值 pivotKey 放到正确的位置 arr[left] ,并返回 left,即基准值在数组中的最终位置。

        qSort 函数是快速排序的递归实现部分。当 left 小于 right 时,调用 partition 函数获取基准值的位置 pivotLoc,然后递归地对基准值左边的子数组 arr[left...pivotLoc - 1] 和右边的子数组 arr[pivotLoc + 1...right] 进行排序,从而实现整个数组的快速排序。

小结

        在这篇博客中,我们可以感受到两种算法的区别。一个就是冒泡排序,它简单直观,通过如上述代码中的标志位优化,在一定程度上能提升效率,但其整体效率仍较低,适用于小规模数据;另一个就是快速排序,虽然高效,但实现相对复杂,适用于大规模数据。

        希望这篇博客对你有所帮助,感谢阅读。

你可能感兴趣的:(数据结构,数据结构,排序算法,算法,c语言)