五花八门的排序算法

        排序算法有很多种,根据情景选取合适的算法可以减少排序消耗的时间,在c++中常用<algorithm>库中的sort函数快排(默认都是从小到大),本文讲解的多种排序实际上包含了多种算法思维,并不是单纯以排序为目的。

冒泡排序(Bubble Sort)

        总是把两个元素中的较小值放在左边,最终构成从小到大。

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

复杂度分析

  • 时间复杂度:O(n^{2}),其中n是数组的长度。这种排序是纯粹的硬排,逐个枚举。 
  • 空间复杂度:O(1),只在交换时加入中间量。

选择排序(Selection Sort)

        寻找没有排序部分的最小值放在已排序的末尾。

	for (int i = 0; i < len - 1; i++) {
		for (int j = 1 + i; j < len; j++) {
			if (arr[i] > arr[j]) {
				int temp = arr[j];
				arr[j] = arr[i];
				arr[i] = temp;
			}
		}
	}
  • 复杂度分析
    • 时间复杂度:O(n^{2})。 
    • 空间复杂度:O(1)。

插入排序(Insertion Sort)

        单个元素不用排序,所以一直从第二个元素开始执行,用key记录元素,之后元素在数组中的位置就被征用了,之后从已排序的最后一个元素向前找,如果比key大就右移,如果没有地方能插入(即为当前最大值)放回原地。

void insertionSort(int arr[], int n) {
    for (int i = 1; i < n; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

 复杂度分析

  • 时间复杂度:平均和最坏情况为 O(n^{2}),最好情况(数组已经有序)为O(n) 。
  • 空间复杂度:O(1)。

     归并排序(Merge Sort) 

            采用分治法(Divide and Conquer)的思想来实现排序功能。归并排序的核心思想是将一个大问题分解为多个小问题,分别解决这些小问题,然后将小问题的解合并起来得到大问题的解。具体到排序问题上,归并排序把一个无序的数组不断地分成两个子数组,直到每个子数组只有一个元素(此时子数组自然是有序的),然后将这些有序的子数组合并成一个最终的有序数组。

    算法步骤

    • 分(Divide):找到数组的中间位置,将数组分成左右两个子数组。递归地对左右两个子数组再“分” ,直到子数组中只有一个元素。因为单个元素的数组可以认为是有序的。
    • 合(Merge):合并两个有序的子数组为一个新的有序数组。比较两个子数组的元素,依次选取较小的元素放入新数组中。重复合并操作,直到所有子数组合并成一个完整的有序数组。

    我们可以用满二叉树 来看待这个过程,原第一代数组等分为第二代,第二代的两个又分成4个第三代,就像细胞分裂一样,最终的第四代就只剩一个元素了(假定一代有8个元素,总之最后就是第n代时,每个叶节点只剩1个元素了),这样就是递归分到底了。

    接了下来进行回溯了,也就是合的过程,最末代的一组的两个数进行大小比较排序,回溯到上一代就是4个数进行排序,直到回溯到第一代,这时候多次局部有序化已经使得整个数组有序了。

     

    代码实现: 

    分的过程直接递归实现,合的过程我们我们建立一个合并函数(merge),如果left>=right了说明已经分到最后一代了,每个节点只有一个元素了。

    // 合并两个已排序的子数组
    void merge(std::vector<int>& arr, int left, int mid, int right) {
        int n1 = mid - left + 1;
        int n2 = right - mid;
        std::vector<int> L(n1), R(n2);// 创建临时数组
        // 拷贝数据到临时数组 L[] 和 R[]
        for (int i = 0; i < n1; i++)
            L[i] = arr[left + i];
        for (int j = 0; j < n2; j++)
            R[j] = arr[mid + 1 + j];
        // 归并临时数组到 arr[left..right]
        int i = 0; // 初始化第一个子数组的索引
        int j = 0; // 初始化第二个子数组的索引
        int k = left; // 初始归并子数组的索引
        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                arr[k] = L[i];
                i++;
            } else {
                arr[k] = R[j];
                j++;
            }
            k++;
        }
        while (i < n1) {// 拷贝 L[] 的剩余元素
            arr[k] = L[i];
            i++;
            k++;
        }
        while (j < n2) {// 拷贝 R[] 的剩余元素
            arr[k] = R[j];
            j++;
            k++;
        }
    }
    // 归并排序函数
    void mergeSort(std::vector<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(n log n) ,其中n是数组的长度。每次将数组分成两半需要O(log n)的时间,而合并每一层两个子数组需要O(n)的时间,因此总的时间复杂度为O(n log n)  。n/2^{k}=1,k=log_{2}n,一共要分k次。
    • 空间复杂度:归并排序的空间复杂度为O(n) ,主要是因为在合并过程中需要额外的临时数组来存储合并结果。

    优点:

    1. 时间复杂度稳定:最坏情况下仍为 O (n log n),适合大规模数据排序;
    2. 稳定排序:可保留相等元素的原始顺序,适合对有序性有要求的场景(如对象排序);
    3. 适合外部排序:无需随机访问元素,仅需顺序读写,可处理磁盘等外部存储中的大数据(如归并排序用于数据库索引排序)。

    缺点:

    1. 空间开销大:需要 O (n) 的辅助数组,不适用于内存受限的场景;
    2. 小规模数据效率低:递归 / 合并的额外开销比插入排序、冒泡排序等简单排序高。

    快速排序(Quick Sort)

    • 原理:采用分治法,选择一个基准值(pivot),将数组分为两部分,使得左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于等于基准值,然后分别对左右两部分递归地进行排序。
    • 代码实现
      void swap(int* a, int* b) {int t = *a;*a = *b;*b = t;}// 交换两个元素的函数
      int partition(int arr[], int low, int high) {// 分区函数
          int pivot = arr[high],i = low;
          for (int j = low; j <= high - 1; j++) {
              if (arr[j] < pivot) {
                  swap(&arr[i++], &arr[j]);
              }
          }
          swap(&arr[i], &arr[high]);
          return i;
      }
      void quickSort(int arr[], int low, int high) {// 快速排序函数
          if (low < high) {
              int pi = partition(arr, low, high);
              quickSort(arr, low, pi - 1);
              quickSort(arr, pi + 1, high);
          }
      }

      取一个基准值,将元素划分(不均分),分区函数是每次区这个区间最右侧的为基准值,j是遍历到右边倒数第二个,i初始是区间最左,如果遍历到arr【j】比基准值小,和arr【i】交换位置,这样小的就在左侧了,遍历完后把基准值放在arr【i】的位置(遍历完了之后的此刻,arr【i】是比基准值大的数),再返回基准值(这一分界线)的索引,然后使用递归的方法,越分区间越小,最终有序化。

    • 复杂度分析
      • 时间复杂度:平均情况为 O(n log n),最坏情况为 O(n^{2})。
      • 空间复杂度:平均情况为O(log n) ,最坏情况为 O(n)。主要是递归调用栈的空间开销。

    快速排序是最最最常用的方法

     桶排序(Bucket Sort)

    • 原理: 桶排序(Bucket Sort)是一种非比较型的排序算法,其核心思想是将待排序的数据分到有限数量的桶里,每个桶再分别进行排序(通常会采用其他排序算法,如插入排序、快速排序等),最后把各个桶中的数据按照顺序合并起来,从而得到一个有序的序列。

    • 实现步骤

    1. 确定桶的数量和范围:根据待排序数据的范围和分布,确定合适的桶数量,计算每个桶能够容纳的数据范围。
    2. 将数据分配到桶中:遍历待排序数组,根据数据的值将其放入对应的桶中。
    3. 对每个桶内的数据进行排序:可以选择合适的排序算法对每个桶内的数据进行排序。
    4. 合并所有桶中的数据:按桶的顺序依次取出每个桶中的数据,组成最终的有序数组。

    代码实现: 

    // 桶排序函数
    void bucketSort(vector<double> &arr) {
        if (arr.empty()) return;
        // 找到数组中的最小值和最大值(<algorithm>内的函数返回的是索引,不是常规max中的值)
        double min_val = *min_element(arr.begin(), arr.end());
        double max_val = *max_element(arr.begin(), arr.end());
        int num=1,bucket_num = arr.size()/num; // 确定桶的数量,这里简单设置为数组的长度
        vector<vector<double> > buckets(bucket_num);// 创建桶
        double bucket_range = (max_val - min_val) / bucket_num;// 计算每个桶的范围
        // 将数据分配到桶中
        for (double num: arr) {
            int index = static_cast<int>((num - min_val) / bucket_range);
            if (index == bucket_num) {
                index = bucket_num - 1;
            }
            buckets[index].push_back(num);
        }
        // 对每个桶内的数据进行排序
        for (auto &bucket: buckets) {
            sort(bucket.begin(), bucket.end());
        }
        // 合并所有桶中的数据
        int index = 0;
        for (const auto &bucket: buckets) {
            for (double num: bucket) {
                arr[index++] = num;
            }
        }
    }
    •  首先如果已知0=<n<=100000啥啥啥的,直接从把范围划分成【0,100000】即呆还占内存,所以准备工作第一步先找出最大值和最小值的索引,之后确定一个桶的数量,这个比较灵活,需要自己判断决定,决定了桶的数量之后每个桶的范围也就确定了,
    • 接下下来进行分桶,逐个遍历确认所在范围入桶就行了,为了保险可以加一个保护措施(实际上多数情况下总范围不能整除桶数,余数都加在最后一个桶上了,有时候可能会桶越界,简单说就是最后一个桶是1.几)

    分桶之后,对每个桶进行桶内排序,局部有序化,由于是按范围分桶,所有整体也是有序的。

    所有只需要简单组合到一个数组中就行了。 

    复杂度分析

    • 时间复杂度
      • 平均情况下,桶排序的时间复杂度为 O(n+k),其中 n是数组的长度,k是桶的数量。当k接近n时,时间复杂度接近线性。
      • 最坏情况下,所有数据都被分配到同一个桶中,此时时间复杂度退化为 O(n^{2}),这取决于桶内排序算法的复杂度。
    • 空间复杂度:桶排序的空间复杂度为O(n+k) ,主要用于存储桶和最终的有序数组。

    适用场景

    桶排序适用于数据分布均匀且数据范围不是特别大的场景,例如对一些浮点数进行排序,或者对年龄、成绩等具有一定范围的数据进行排序。

    从这个例子中我们不难发现,桶排经常还快速排序sort协调排序

    我们用两种方法排序并得到了以下结果(实际结果可能因机器性能而异):

    nBucket Sort (s)Quick Sort (s)
    100.0000120.000008
    1000.0000450.000032
    10000.0003500.000400
    100000.0032000.004500
    1000000.0350000.050000
    10000000.4000000.600000

     桶排是一种以空间换取时间的排序,快速排序在处理大量数据时耗时过多,所有可以使用桶排的思想分批快速排序,减少排序耗时。

    我们给出的代码实际上是桶优化的快排,这里我们再说一下最基本的桶排序:

    例如,对n个小于100000个int型数据进行排序

    vector<int> v(n, 0),arr(100000,0),ans;
        for (int i = 0; i < n; i++) {cin >> v[i];arr[v[i]]++;}
        for (int i = 0; i < 100000; i++) {
            while(arr[i]--) {ans.push_back(i);}
        }

    每个小于100000的数自成一个桶,统计出现个数然后按序加入数字就能实现排序,只是比较消耗内存,这里v数组也就是原数组实际上可以省略,那么每个元素都会被操作2次,一次统计,一次合并,这是一个O(n)的排序。

    posted @ 2025-02-07 15:09  mc12356  阅读(16)  评论(0)    收藏  举报  来源