C/C++八大排序

排序

排序有内部排序外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

image-20230620141451779

按照难易程度排序,八大排序算法可以从简单到复杂依次排列如下:

  1. 冒泡排序(Bubble Sort)
  2. 选择排序(Selection Sort)
  3. 插入排序(Insertion Sort)
  4. 希尔排序(Shell Sort)
  5. 堆排序(Heap Sort)
  6. 计数排序(Counting Sort)
  7. 归并排序(Merge Sort)
  8. 快速排序(Quick Sort)

冒泡排序、选择排序和插入排序是最简单的三种排序算法,易于理解和实现,但是它们的时间复杂度较高,对于大规模数据的排序效率不高。

希尔排序是一种改进的插入排序,它通过分组插入排序来提高效率,但是希尔排序的时间复杂度也比较高。

堆排序、计数排序和归并排序是比较复杂的排序算法,需要理解和掌握更多的算法知识和技巧,但是它们的时间复杂度都比较优秀,适用于大规模数据的排序。

快速排序是最复杂的一种排序算法,它需要对递归和分治的思想有深刻的理解和应用,但是快速排序是目前应用最广泛的排序算法之一,具有较高的排序效率和稳定性。

冒泡排序(Bubble Sort)

冒泡排序得本质在于交换,即每次通过交换的方式把当前剩余元素的最大值移动到一端,而当剩余元素减少为0时,排序结束。

冒泡排序:其基本思想是通过重复遍历待排序的元素列表,比较相邻元素的大小,并根据需要交换它们的位置,使得每一趟遍历都能将最大(或最小)的元素"冒泡"到列表的末尾。这个过程就像是气泡在水中冒泡一样,较大(或较小)的元素逐渐往后移动。

每一趟遍历都会确定一个最大(或最小)的元素的最终位置,因此需要执行 n-1 趟遍历来完成排序,其中 n 是待排序列表的长度。

时间复杂度O(\(n^2\)),空间复杂度O(1)

void Bubble(int *arr, int len)
{
    //1、如果有len个数据,则需要扫描(len - 1)趟
    for(int i = 1; i <= len - 1; ++i) {
    //2、每一趟都要从头开始扫描,直至扫描到末尾,将扫描中遇到的最大数放在末尾位置
        for(int j = 0; j < len - i; ++j){ //注意:每一趟扫描的数据都会减少,所以时(len-i)
            if(arr[j] > arr[j + 1]){ //因为 j<len-1,所以j+1不会越界
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

需要注意的是,冒泡排序是一种简单但效率较低的排序算法,尤其对于大规模的数据集,其时间复杂度为O(n^2),其中n为待排序元素的个数。因此,在实际应用中,当数据规模较大时,通常会选择其他更高效的排序算法。

选择排序(Selection Sort)

选择排序:其基本思想是每次从未排序部分选择最小(或最大)的元素,并将其放置在已排序部分的末尾。通过不断选择最小(或最大)元素,逐步构建有序序列。

具体步骤如下:(以从小到大排序为例)

  1. 将待排序的第一个元素作为头元素,已排序列表还没构建。
  2. 将待排序列表的头元素作为比较元素,与待排序列表的其他元素进行遍历比较;若头元素是该列表中的最小元素则位置不动;若头元素不是最小元素,则将最小元素与头元素交换位置,让最小元素成为头元素。
  3. 将待排序列表头元素视为已排序列表的尾元素,待排序列表失去原先的头元素。
  4. 重复步骤2和步骤3,直到全部待排序的数据元素排完。

时间复杂度O(\(n^2\)),空间复杂度O(1)

void SelectSort(int* arr, const int len)
{
    // 遍历执行N趟,每一趟都是先取出待排序列表的第一个元素暂作为最小值记录
	for (int i = 0; i < len; i++)
	{
		int min = i;
        //对待排序列表中的其他元素进行遍历,查找比min更小的值
		for (int j = i + 1; j < len; j++)
		{
			if (arr[j] < arr[min]){//更新最小值的位置
				min = j; //更新最小值位置
            }
		}
        //遍历完,发现待排序列表的第一个元素不是最小元素
		if (i != min){
            //将待排序列表中的第一个元素与最小元素进行交换
			swap(arr + i, arr + min);
        }
        //之后已排序列表壮大,未排序列表短小
	}
}

需要注意的是,选择排序的时间复杂度为O(\(n^2\)),其中n为待排序元素的个数。虽然选择排序的效率相对较低,但由于其简单直观的实现方式,对于小型数据集或者部分已排序的列表,选择排序仍然可以是一个可选的排序算法。

插入排序(Insertion Sort)

插入排序:其基本思想是将待排序的元素逐个插入到已排序部分的合适位置,通过不断地插入操作,从而逐步构建有序序列。

具体步骤如下:(以从小到大排序为例)

  1. 将待排序的第一个元素视为已排序列表,从第二个元素开始视为待排序列表。
  2. 将待排序列表的头元素作为待插入元素,与已排序列表的末尾向前遍历比较,将已排序列表中比待插入元素大的元素向右移动。
  3. 直到找到合适的位置插入,待排序列表失去原先的头元素。
  4. 重复步骤2和步骤3,直到所有元素都被插入到已排序部分。

时间复杂度O(\(n^2\)),空间复杂度O(1)

void InsertionSort(int arr[], int len) {
    
    for (int i = 1; i < len; i++) { //执行len-1个元素进行排序
        int key = arr[i]; //将待排序的头元素作为待插入元素
        int j = i - 1; //j指向已排序的最后一个元素
        
        // 如果已排序的尾元素大于待插入元素
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j]; //将已排序中比待插入元素大的元素往右移动
            j--;
        }
        //如果已排序的某个元素小于等于待插入元素,arr[j] <= key
        arr[j + 1] = key; //待插入元素插入到该元素的后面
    }
}

相比其他复杂的排序算法,插入排序的实现较为简单,适用于小规模的数据排序或已经部分有序的列表。

插入排序的时间复杂度为O(n^2),其中n为待排序元素的个数。它是一个稳定的排序算法,适用于各种类型的数据,包括数字、字符串等。此外,插入排序是原地排序算法,不需要额外的空间来存储临时数据,只需要少量的额外变量用于元素比较和交换操作。

希尔排序(Shell Sort)

希尔排序(Shell Sort),也称作缩小增量排序,是插入排序的一种改进算法。它通过将待排序的元素按照一定的间隔(称为增量)分组,对每个分组进行插入排序,然后逐渐缩小增量,重复这个过程,直到增量为1,最终完成整个序列的排序。

选择gap的大小使得原始数据更加有序,当gap=1的时候就是插入排序。

具体步骤如下:(以从小到大排序为例)

  1. 首先,选择一个增量序列,也称为间隔序列(例如:Knuth增量序列、Sedgewick增量序列等)。
  2. 根据选定的增量序列,将待排序的元素分为若干个分组,每个分组包含相隔一定增量的元素。
  3. 对每个分组应用插入排序,也就是将每个分组内的元素进行插入排序,使得每个分组内的元素部分有序。
  4. 缩小增量,重复步骤2和步骤3,直到增量为1。最后一次增量为1时,即进行一次普通的插入排序。
  5. 完成上述步骤后,整个序列将被排序成最终的有序序列。

平均情况下在 O(nlogn) 到 O(\(n^2\))之间。它的空间复杂度为 O(1)

void ShellSort(int arr[], int len) {
    // 选择增量序列,可以根据不同的增量序列进行调整
    for (int gap = len / 2; gap > 0; gap /= 2) {
        // 对每个增量组进行插入排序
        for (int i = gap; i < len - gap; i++) { 执行len-gap个元素进行排序
            int temp = arr[i]; //待排序增量组头元素
            int j = i - gap;  //j指向已排序增量组的尾元素

            // 在当前增量下,进行插入排序
            while (j >= 0 && arr[j] > temp) {
                arr[j + gap] = arr[j]; //将已排序中比待插入元素大的元素往右移动
                j -= gap;
            }
			//待插入元素插入到该元素的gap增量的后面
            arr[j + gap] = temp;
        }
    }
}

image-20230616155652959 image-20230616155948403

希尔排序利用了插入排序在部分有序序列上的高效性质,通过分组的方式,先使序列部分有序,然后逐渐减小增量,最终达到完全有序的目的。

希尔排序的时间复杂度取决于增量序列的选择,对于一些特定的增量序列,希尔排序的时间复杂度可以达到O(n log n)。然而,希尔排序的时间复杂度的确切分析比较复杂,因为增量序列的选择会影响排序的效率。希尔排序是原地排序算法,只需要少量的额外空间来进行元素交换,空间复杂度为O(1)。

总体而言,希尔排序是一种改进的插入排序算法,通过分组和逐渐缩小增量的方式,提高了插入排序在大规模数据上的效率。它相对简单,并且在实践中表现出良好的性能,特别是对于中等大小的数据集合。

堆排序(Heap Sort)

关于堆的介绍

堆排序:核心思想是通过构建和调整堆来实现排序。通过将待排序数组转化为最大堆(或最小堆),并重复交换堆顶元素和末尾元素,并调整堆,最终实现排序的目的。

具体步骤如下:(以从小到大排序为例)

  1. 构建最大堆:将待排序的数组看作是一个完全二叉树,并通过一系列的比较和交换操作,将其调整为最大堆。这样最大元素就位于根节点。
  2. 交换堆顶元素和末尾元素:将堆顶元素(最大元素)与数组末尾元素进行交换,这样最大元素就移动到了数组的末尾位置。
  3. 调整堆:对交换后的堆顶元素进行调整,重新构建最大堆,再次将最大元素置于堆顶。
  4. 重复交换和调整步骤:重复执行步骤2和步骤3,直到所有元素都被放置在正确的位置。
// 交换数组中两个元素的值
void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 调整堆,使其满足最大堆性质
void heapify(int arr[], int n, int i) {
    int largest = i;     // 假设当前节点 i 是最大值
    int left = 2 * i + 1;    // 左子节点的索引
    int right = 2 * i + 2;   // 右子节点的索引

    // 如果左子节点大于根节点,将 largest 更新为左子节点
    if (left < n && arr[left] > arr[largest])
        largest = left;

    // 如果右子节点大于根节点,将 largest 更新为右子节点
    if (right < n && arr[right] > arr[largest])
        largest = right;

    // 如果 largest 不是根节点,则交换根节点与 largest 位置的元素,并递归调整堆
    if (largest != i) {
        swap(&arr[i], &arr[largest]);
        heapify(arr, n, largest);
    }
}

// 堆排序函数
void heapSort(int arr[], int n) {
    // 构建最大堆
    for (int i = n / 2 - 1; i >= 0; i--) //第一个非叶子结点 n/2-1
        heapify(arr, n, i);

    // 从最后一个元素开始,依次将其与堆顶元素交换并重新调整堆
    for (int i = n - 1; i > 0; i--) {
        swap(&arr[0], &arr[i]);
        heapify(arr, i, 0);
    }
}


堆排序的关键在于构建和调整堆的过程。通过不断地交换和调整堆,可以保证每次交换后,堆的性质仍然得以保持,直到所有元素都被放置在正确的位置。

堆排序的时间复杂度为O(nlogn),其中n是待排序数组的长度。堆排序是一种原地排序算法,不需要额外的辅助空间。然而,由于其频繁的元素交换操作,相比于其他排序算法,它的性能可能略低。但堆排序具有稳定的时间复杂度,适用于大规模数据的排序。

计数排序(Counting Sort)

计数排序(Counting Sort)是一种线性时间复杂度的排序算法,适用于对一定范围内的整数进行排序。

计数排序的基本思想是通过统计每个元素出现的次数,然后根据统计信息将元素排列到正确的位置上。算法的步骤如下:

  1. 找出待排序数组中的最大值max和最小值min,确定统计数组的长度 range = max - min + 1。
  2. 创建一个长度为range的辅助数组count,用于记录每个元素出现的次数。
  3. 遍历待排序数组,统计每个元素出现的次数,将其存储在count数组中。例如,如果待排序数组中有3个元素的值为5,那么count[5]的值将为3。
  4. 对count数组进行累加操作,使得count[i]表示小于等于元素 i 的元素个数。
  5. 创建一个与待排序数组长度相同的结果数组result。
  6. 从待排序数组末尾开始遍历,根据count数组找到待排序元素在结果数组中的正确位置,并将元素存储在该位置上。
  7. 重复步骤6直到遍历完待排序数组。

通过以上步骤,计数排序会将待排序数组中的元素按照从小到大的顺序排列在结果数组中。

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

void countingSort(int arr[], int n) {
    // 查找最大值和最小值
    int max = arr[0], min = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
        if (arr[i] < min) {
            min = arr[i];
        }
    }

    // 创建计数数组并初始化为0
    int range = max - min + 1;
    int* count = (int*)calloc(range, sizeof(int));

    // 统计每个元素出现的次数
    for (int i = 0; i < n; i++) {
        count[arr[i] - min]++; //偏移量就是arr[i] - min
    }

    // 计算累加次数,使count[i]表示小于等于元素i的元素个数,该位置的值后面有几个元素
    for (int i = 1; i < range; i++) {
        count[i] += count[i - 1];
    }

    // 创建结果数组
    int* result = (int*)malloc(n * sizeof(int));

    // 从原数组末尾开始遍历,根据count数组找到元素的正确位置,存储到结果数组中
    for (int i = n - 1; i >= 0; i--) {
        result[--count[arr[i] - min]] = arr[i];
    }

    // 将结果数组拷贝回原数组
    for (int i = 0; i < n; i++) {
        arr[i] = result[i];
    }

    // 释放内存
    free(count);
    free(result);
}



计数排序的时间复杂度为O(n + k),其中n为待排序数组的长度,k为统计数组的长度。尽管计数排序具有线性时间复杂度,但它的适用范围受限,主要取决于待排序元素的范围。当待排序元素范围较大且元素数量较少时,计数排序是一个高效的排序算法。然而,如果待排序元素范围很大或者是负数,计数排序的空间复杂度可能较高。

归并排序(Merge Sort)

归并排序(Merge Sort)是一种基于分治思想的排序算法,它的基本思想是将待排序数组不断划分成更小的子数组,直到划分到单个元素,然后再将这些单个元素合并成有序的数组。

归并排序的步骤如下:

  1. 分解(Divide):将待排序数组不断划分为更小的子数组,直到划分到单个元素。可以通过递归的方式实现,每次将数组划分为两个近似相等的子数组。
  2. 合并(Merge):将两个有序的子数组合并成一个有序的数组。合并的过程是通过比较两个子数组的元素,并按照从小到大的顺序依次放入结果数组中。
  3. 递归合并(Recursive Merge):重复进行步骤2,递归地将更小的子数组合并成有序的数组,直到所有的子数组合并完毕,得到最终的有序数组。

归并排序的关键操作是合并(Merge)过程。合并操作需要创建一个临时数组,用于存储合并后的有序结果。该操作通过比较两个子数组的元素,并按照顺序将较小的元素放入临时数组中,直到其中一个子数组的元素全部放入临时数组中。然后,将剩余的子数组中的元素依次放入临时数组的末尾。最后,将临时数组中的元素复制回原始数组的相应位置。

#include <stdio.h>

// 合并两个有序子数组
void merge(int arr[], int left, int mid, int right) {
    int i, j, k;
    int n1 = mid - left + 1;
    int n2 = right - mid;

    // 创建临时数组
    int L[n1], R[n2];

    // 将数据复制到临时数组
    for (i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];

    // 合并临时数组的元素到原数组
    i = 0; // L数组的索引
    j = 0; // R数组的索引
    k = left; // 原数组的索引

    while (i < n1 && j < n2) {
        arr[k++] = L[i] <= R[j] ? L[i++] : R[j++];
    }

    // 复制剩余元素到原数组
    while (i < n1) {
        arr[k++] = L[i++];
    }

    while (j < n2) {
        arr[k++] = R[j++];
    }
}

// 归并排序
void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;

        // 分割并递归排序左半部分和右半部分
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);

        // 合并排序后的两个子数组
        merge(arr, left, mid, right);
    }
}

归并排序的时间复杂度为O(nlogn),其中n为待排序数组的长度。它具有稳定性,适用于各种数据类型的排序任务。然而,归并排序需要额外的存储空间来存储临时数组,在空间复杂度上稍高。

快速排序(Quick Sort)

快速排序的基本思想如下:

  1. 选择一个基准元素(pivot),一般选择序列的第一个元素或最后一个元素;
  2. 将序列中所有比基准元素小的元素放在基准元素的左边,所有比基准元素大的元素放在基准元素的右边;
  3. 对基准元素左边和右边的两个子序列重复执行步骤1和步骤2,直到每个子序列只剩下一个元素或为空。
void Sort(int arr[], int left,int right) {
	if(left >= right) return;
	int i = left, j = right, prvot = arr[left];
	while(i != j){
		while(arr[i] < prvot && i < j){
			i++;
		}
		while(arr[j] > prvot && i < j){
			j--;
		}
		if(i < j){
			swap(&arr[i],&arr[j]);
		}
	}
	swap(&arr[left],&arr[i]);
	Sort(arr,left,i-1);
	Sort(arr,i+1,right); 
}

快速排序的时间复杂度为O(nlogn),但是最坏情况下(即数组已经有序或基本有序时),时间复杂度为O(n^2)。快速排序是一种原地排序(in-place sorting),不需要额外的存储空间。

快速排序的优点是效率高,适用于处理大规模的数据;缺点是在最坏情况下时间复杂度较高,且不稳定(即可能改变相等元素的相对顺序)。

posted @ 2023-07-17 20:51  雪国北风  阅读(43)  评论(0编辑  收藏  举报