排序算法


排序算法稳定性

常用排序稳定性

我暂时还没具体分析锦标赛排序为什么是不稳定的。

Stable Sorting Algorithms:

  • Insertion Sort、Merge Sort、Bubble Sort、Tim Sort、Counting Sort、Block Sort、Quadsort、Library Sort、Cocktail shaker Sort、Gnome Sort、Odd–even Sort

Unstable Sorting Algorithms:

  • Heap sort、Selection sort、Shell sort、Quick sort、Introsort (subject to Quicksort)、Tree sort、Cycle sort、Smoothsort、Tournament sort(subject to Heapsort)
排序算法 平均时间复杂度 最坏时间复杂度 最好时间复杂度 空间复杂度 稳定性
冒泡排序 O(n²) O(n²) O(n) O(1) 稳定
直接选择排序 O(n²) O(n²) O(n) O(1) 不稳定
直接插入排序 O(n²) O(n²) O(n) O(1) 稳定
快速排序 O(nlogn) O(n²) O(nlogn) O(nlogn) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不稳定
希尔排序 O(nlogn) O(ns) O(n) O(1) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
计数排序 O(n+k) O(n+k) O(n+k) O(n+k) 稳定
基数排序 O(N*M) O(N*M) O(N*M) O(M) 稳定

交换的实现

template <typename T>
void Swap(T &a, T &b) {
  T temp = a;
  a = b;
  b = temp;
}

简单选择排序

它的工作原理是每次找出第 \(i\) 小的元素(也就是 \(A_{i\dots n}\) 中最小的元素),然后将这个元素与数组第 \(i\) 个位置上的元素交换。

template <typename T>
void selection_sort(std::vector<T> &arr) {
  int len = arr.size();
  for (int i = 0; i < len; ++i) {
    int idx = i;
    for (int j = i + 1; j < len; ++j) {
      if (arr[j] < arr[idx]) idx = j;  // 此处需要类型T重载"<"运算符
    }
    std::swap(arr[i], arr[idx]);
  }
}

冒泡排序

它的工作原理是每次检查相邻两个元素,如果前面的元素与后面的元素满足给定的排序条件,就将相邻两个元素交换。经过 \(i\) 次扫描后,数列的末尾 \(i\) 项必然是最大的 \(i\) 项,因此冒泡排序最多需要扫描 \((n-1)\) 遍数组就能完成排序。 [ 冒泡排序的两点优化 ]

template <typename T>
void bubble_sort_opt(std::vector<T> &arr) {
  int len = arr.size();
  bool swapped = true;
  while (swapped) {
    swapped = false;
    for (int i = 1; i < len; ++i) {
      if (arr[i] < arr[i - 1]) {
        swapped = true;
        std::swap(arr[i - 1], arr[i]);
      }
    }
    // --len; // 可优化,第i轮扫描可以确定前i大的数字
  }
}

插入排序

它的工作原理为将待排列元素划分为“已排序”和“未排序”两部分,每次从“未排序的”元素中选择一个插入到“已排序的”元素中的正确位置。

template <typename T>
void insertion_sort(std::vector<T> &arr) {
  int len = arr.size();
  for (int i = 1; i < len; ++i) {
    T key = arr[i];
    int idx = i - 1;
    while (idx >= 0 && key < arr[idx]) {
      arr[idx + 1] = arr[idx];
      --idx;
    }
    arr[idx + 1] = key;  //可知arr[idx] <= key, 插入位置为idx + 1
  }
}
template <typename T>
void insertion_sort(std::vector<T> &arr) {
  int len = arr.size();
  if (len <= 1) return;
  for (int i = 1; i < len; ++i) {
	for (int j = i; j >= 1 && arr[j] < arr[j-1]; --j) {
      std::swap(arr[j], arr[j-1]);
    }
  }
}

快速排序

快排,又被称作分区交换排序(partition-exchange sort),基本思想是分治,步骤如下:

  1. 将数列划分为两部分(要求保证相对大小关系);
  2. 递归到两个子序列中分别进行快速排序;
  3. 不用合并,因为此时数列已经完全有序。
template <typename T>
int partition(std::vector<T> &arr, int low, int high) {
  T key = arr[low];
  while (low < high) {
    // 先处理右侧
    while (low < high && arr[high] >= key) --high;
    arr[low] = arr[high];
    // 处理左侧
    while (low < high && arr[low] <= key) ++low;
    arr[high] = arr[low];
  }
  arr[low] = key;

  return low;
}

template <typename T>
void quick_sort(std::vector<T> &arr, int low, int high) {
  if (low < high) {
    int idx = partition(arr, low, high);
    quick_sort(arr, low, idx - 1);
    quick_sort(arr, idx + 1, high);
  }
}

快排的优化技巧:随机化选取key,取三个key的中间数,待排序序列规模较小时使用插入排序


归并排序

归并排序分为三个步骤:

  1. 将数列划分为两部分;
  2. 递归地分别对两个子序列进行归并排序;
  3. 合并两个子序列。
// 递归写法
template <typename T>
void merge(std::vector<T> &arr, int low, int mid, int high) {
  std::vector<T> tmp_arr;
  tmp_arr.reserve(high - low + 1); // 其实这里还不是很完美
  int left_idx = low;
  int right_idx = mid + 1;
  while (left_idx <= mid && right_idx <= high) {
    if (arr[left_idx] < arr[right_idx]) {
      tmp_arr.push_back(arr[left_idx]);
      ++left_idx;
    } else {
      tmp_arr.push_back(arr[right_idx]);
      ++right_idx;
    }
  }
  while (left_idx <= mid) {
    tmp_arr.push_back(arr[left_idx]);
    ++left_idx;
  }
  while (right_idx <= high) {
    tmp_arr.push_back(arr[right_idx]);
    ++right_idx;
  }
  std::cout << tmp_arr.size() << " " << tmp_arr.capacity() << std::endl;
  for (int i = 0; i < high - low + 1; ++i) arr[low + i] = tmp_arr[i];
}

template <typename T>
void merge_sort(std::vector<T> &arr, int low, int high) {
  if (low < high) {
    int mid = low + (high - low) / 2;
    merge_sort(arr, low, mid);
    merge_sort(arr, mid + 1, high);
    merge(arr, low, mid, high);
  }
}
// 迭代写法
template <typename T>
void merge_sort2(std::vector<T> &arr, int low, int high) {
  int len = high - low + 1;
  std::vector<T> tmp_arr;
  tmp_arr.reserve(len);
  for (int seg = 1; seg < len; seg += seg) {
    for (int idx = 0; idx < len; idx += seg * 2) {
      int left = idx;
      int mid = std::min(idx + seg, len);
      int right = std::min(idx + seg * 2, len);
      int tmp_idx = left;
      int idx1 = left;
      int idx2 = mid;
      // 归并
      while (idx1 < mid && idx2 < right) {
        tmp_arr[tmp_idx++] =
            (arr[idx1] < arr[idx2]) ? arr[idx1++] : arr[idx2++];
      }
      while (idx1 < mid) tmp_arr[tmp_idx++] = arr[idx1++];
      while (idx2 < high) tmp_arr[tmp_idx++] = arr[idx2++];
    }
    std::swap(arr, tmp_arr);
  }
}

[ 归并排序 - 维基百科 ]


堆排序

数组从小到大排序应当建立大顶堆:将堆顶的元素取出,作为最大值,与数组尾部的元素交换,并维持残余堆的性质;之后将堆顶的元素取出,作为次大值,与数组倒数第二位元素交换,并维持残余堆的性质;以此类推,在第 \(n-1\) 次操作后,整个数组就完成了排序。

在STL中,优先队列priority_queue默认是小顶堆,所以输出序列是从大到小排序。

template <typename T>
void sift_down(std::vector<T> &arr, int low, int high) {
  int parent = low;
  int child = parent * 2 + 1;
  while (child <= high) {
    if (child + 1 <= high && arr[child] < arr[child + 1]) {  // 小顶堆改成arr[child] > arr[child + 1]
      ++child;
    }
    if (arr[parent] >= arr[child]) {  //  小顶堆改成arr[parent] <= arr[child]
      break;
    } else {
      std::swap(arr[parent], arr[child]);
      parent = child;
      child = parent * 2 + 1;
    }
  }
}

template <typename T>
void heap_sort(std::vector<T> &arr) {
  int len = arr.size();
  // 建堆
  for (int i = len / 2 - 1; i >= 0; --i) sift_down(arr, i, len - 1);
  // 排序, 将堆顶元素与第i个元素 (当前堆的最后一个) 交换位置
  for (int i = len - 1; i > 0; --i) {
    std::swap(arr[0], arr[i]);
    sift_down(arr, 0, i - 1);
  }
}

希尔排序

希尔排序又称递减增量排序算法,具体介绍见 [ 希尔排序 - 维基百科 ]

template <typename T>
void shell_sort(std::vector<T> &arr, int low, int high) {
  int len = high - low + 1;
  for (int step_size = len >> 1; step_size > 0; step_size >>= 1) {
    for (int i = step_size; i < len; ++i) {
      for (int j = i; j >= step_size && arr[j] < arr[j - step_size];
           j -= step_size) {
        std::swap(arr[j], arr[j - step_size]);
      }
    }
  }
}

分割线

下面几种排序的重要性较弱。

  • 计数排序适合数据范围不大的一组数进行排序。
  • 基数排序要求数据之间明显存在可进行关键字比较的特征。
  • 桶排序同样适用于数据范围不大的情况,它可以视作对计数排序的改进,减少了空间浪费。

计数排序

它的工作过程分为三个步骤:

  1. 计算每个数出现了几次;
  2. 求出每个数出现次数的 前缀和
  3. 利用出现次数的前缀和,从右至左计算每个数的排名(保证排序算法的稳定性,即相同大小的数字,原先在右侧的仍然在右侧)。
// C++ Version
const int N = 100010;
const int W = 100010;

int n, w, a[N], cnt[W], b[N];

void counting_sort() {
  memset(cnt, 0, sizeof(cnt));
  for (int i = 1; i <= n; ++i) ++cnt[a[i]];
  for (int i = 1; i <= w; ++i) cnt[i] += cnt[i - 1];
  for (int i = n; i >= 1; --i) b[cnt[a[i]]--] = a[i];
}

基数排序

它的工作原理是将待排序的元素拆分为 \(k\) 个关键字(比较两个元素时,先比较第一关键字,如果相同再比较第二关键字……),然后先对第 \(k\) 关键字进行稳定排序,再对第 \((k-1)\) 关键字进行稳定排序,再对第 \((k-2)\) 关键字进行稳定排序……最后对第一关键字进行稳定排序,这样就完成了对整个待排序序列的稳定排序。

==基数排序需要借助一种 稳定算法 完成内层对关键字的排序。==

/* 基数排序
 * 假设所有数都是非负的整数, 关键字的排序使用计数排序
 */

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>

void counting_sort(std::vector<unsigned int> &arr, unsigned int mod) {
  int len = arr.size();
  std::vector<unsigned int> cnt(10, 0);
  std::vector<unsigned int> tmp_arr(len, 0);
  // 计数
  for (auto n : arr) {
    ++cnt[(n / mod) % 10];
  }
  // 求cnt前缀和
  for (int i = 1; i < cnt.size(); ++i) {
    cnt[i] += cnt[i - 1];
  }
  // 自右向左排序
  for (int i = len - 1; i >= 0; --i) {
    int key = (arr[i] / mod) % 10;
    tmp_arr[cnt[key] - 1] = arr[i];
    --cnt[key];
  }
  arr = tmp_arr;
}

void radix_sort(std::vector<unsigned int> &arr) {
  unsigned int max_num = *std::max_element(arr.begin(), arr.end());
  // 取出最大的数用于判断关键字数量,按关键字(0-9)排序,其余较短数字缺位补0
  // 此处关键字比较顺序是从个位开始,向高位比较
  for (unsigned int mod = 1; max_num / mod > 0; mod *= 10) {
    counting_sort(arr, mod);
  }
}

int main() {
  std::vector<unsigned int> arr = {53,  3,  542, 748, 14, 214,
                                   154, 14, 63,  616, 589};
  radix_sort(arr);
  std::copy(arr.begin(), arr.end(),
            std::ostream_iterator<unsigned int>(std::cout, " "));
  return 0;
}

桶排序

工作的原理是将数组分到有限数量的桶里,每个桶再各自排序(配合其它排序算法),步骤如下:

  1. 设置一个定量的数组当作空桶。
  2. 寻访序列,并且把项目逐个放到对应的桶内。
  3. 对每个非空桶进行排序。
  4. 合并所有非空桶内数据,还原成有序序列。

[ 桶排序 - 维基百科 ] 这里有基于链表的桶排序实现。

// 假设只有10个桶,数字范围是[0, 99]
const int NUM_BUCKETS = 10;

template <typename T = int>
void _insertion_sort(std::vector<T> &arr) {
  int len = arr.size();
  if (len <= 1) return;
  for (int i = 1; i < len; ++i) {
    for (int j = i; j >= 1 && arr[j] < arr[j - 1]; --j) {
      std::swap(arr[j], arr[j - 1]);
    }
  }
}

template <typename T = int>
void bucket_sort(std::vector<T> &arr, int low, int high) {
  // 元素分桶
  std::vector<std::vector<T>> buckets(NUM_BUCKETS, std::vector<T>{});
  for (auto n : arr) {
    int idx = n / NUM_BUCKETS;
    buckets.at(idx).push_back(n);
  }
  // 桶内排序, 同时合并结果
  int k = 0;
  for (int i = 0; i < NUM_BUCKETS; ++i) {
    _insertion_sort(buckets.at(i));
    for (auto n : buckets.at(i)) {
      arr[k++] = n;
    }
  }
}


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!