1. 冒泡排序(不稳定)

  1. vector<int> arr = { 1, 2, 3, 4, 5 };
  2. for(int end = arr.size() - 1; end > 0; end--) {
  3. // 从第二个位置开始比,每次和前一个元素比对,一直比对到最后面一个元素
  4. // 这样,每一轮循环之后,最大的元素会在最后
  5. // 同时,下一个循环需要在之后一个元素之前的序列中再次进行,所以,每次排序后最后比对的位置要前移,即end--
  6. for(int begin = 1; begin <= end; begin++) {
  7. if(arr[begin] < arr[begin-1]) {
  8. int tmp = arr[begin];
  9. arr[begin] = arr[begin-1];
  10. arr[begin-1] = tmp;
  11. }
  12. }
  13. }
  14. // 优化
  15. // 以上算法中,每一轮循环都要从第一个到最后一个位置进行遍历
  16. // 那么,如果对于有序的数组来说,依然要进行没有必要的遍历,其实这是浪费的
  17. // 所以,我们可以在当前轮遍历时判断是不是已经有序了,如果已经有序了,直接退出循环即可
  18. vector<int> arr = { 1, 2, 3, 4, 5 };
  19. for(int end = arr.size() - 1; end > 0; end--) {
  20. bool isSorted = true; // 在每一轮循环前,假定是已经有序了
  21. for(int begin = 1; begin <= end; begin++) {
  22. if(arr[begin] < arr[begin-1]) { // 如果发生了交换,说明是无序的
  23. int tmp = arr[begin];
  24. arr[begin] = arr[begin-1];
  25. arr[begin-1] = tmp;
  26. isSorted = false;
  27. }
  28. }
  29. // 如果当前循环没有发生交换,说明数组已经有序了,直接退出循环即可
  30. if(isSorted) break;
  31. }
  32. // 不过,对于一个数组来说,上面已经有序或者排几轮就有序的情况概率比较低
  33. // 最坏情况下 isSorted 根本不起作用,而且对于 CPU 指令来说,还多了几个步骤,相比原始算法还稍微耗时
  34. // 那么如何继续优化呢?
  35. // 可以根据最后几个已经有序的情况来处理
  36. // 如果最后几个已经有序了,那么可以记录下最后一个交换的位置,下一次循环直接到最后一个交换的位置即可
  37. vector<int> arr = { 1, 2, 3, 4, 5 };
  38. for(int end = arr.size() - 1; end > 0; end--) {
  39. bool sortedIndex = 1; // sortedIndex = 1是给已经有序的情况处理的
  40. // 如果已经有序了,for loop 中的 sortedIndex 不会被改值
  41. // 那么 end = sortedIndex = 1,下一个轮次中 end-- == 0 就直接退出了,这不巧了吗
  42. for(int begin = 1; begin <= end; begin++) {
  43. if(arr[begin] < arr[begin-1]) {
  44. int tmp = arr[begin];
  45. arr[begin] = arr[begin-1];
  46. arr[begin-1] = tmp;
  47. sortedIndex = begin;
  48. }
  49. }
  50. // 把最后一次交换的位置赋值给 end ,下一次直接遍历到这个位置即可
  51. end = sortedIndex;
  52. }

2. 选择排序(不稳定)

vector<int> arr = { 1, 2, 3, 4, 5 };
for(int end = arr.size() - 1; end > 0; end--) {
    int maxIndex = 0;
    for(int begin = 1; begin <= end; begin++) {
      if(arr[begin] > arr[maxIndex]) {
        maxIndex = begin;
      }
    }
    int tmp = arr[maxIndex];
    arr[maxIndex] = arr[end];
    arr[end] = tmp;
}

3. 插入排序(稳定)

vector<int> arr = {23, 12, 55, 6, 98};
for(int begin = 1; begin < arr.size(); begin++) {
  int cur = begin;
  while(cur > 0 && arr[cur] < arr[cur - 1]) {
    swap(arr[cur], arr[cur-1]);
    cur--;
  }
}

4. 归并排序(稳定)


void sort(int begin, int end);
void merge(int begin, int mid, int end);

vector<int> arr = {1, 2, 3, 4, 5};
vector<int> leftArray(arr.size() >> 1);
sort(0, arr.size());

void sort(int begin, int end) {
  if(end - begin < 2) { // (递归终止条件)小于2个元素
    return;
  }
  int mid = (begin + end) >> 1;
  // 分成2半,对左边进行排序,对右边进行排序
  // [begin, mid), [mid, end)
  sort(begin, mid);
  sort(mid, end);
  merge(begin, mid, end);
}

// 将 [begin, mid), [mid, end) 范围的序列合并成一个有序序列
void merge(int begin, int mid, int end) {
  int li = 0;
  int le = mid - begin;
  int ri = mid;
  int re = end;
  int ai = begin;

  // 备份左边数组
  for(int i = li; i < le; i++) {
    leftArray[i] = arr[begin + i];
  }

  while(li < le) { // 左边还没结束才执行操作
      if(ri < re && arr[ri] < leftArray[li]) {
      arr[ai] = arr[ri];
      ai++;
      ri++;
    } else {
      arr[ai] = leftArray[li];
      ai++;
      li++;
    }
  }
}

5. 堆排序(不稳定)

vector<int> arr = {1, 2, 3, 4, 6, 7};
int heapSize = arr.size();

// 1. 建堆,采用自下而上的下溢
// (size >> 1) - 1 : 最后一个非叶子节点的索引
// 完全二叉树非叶子节点为 floor(n/2) 即 总数的一半
// 即第一个叶子节点的索引为 size 的一半,所以(size >> 1) - 1 : 最后一个非叶子节点的索引
for(int i = (heapSize >> 1) - 1; i >= 0; i--) {
  siftDown(i);
}

void siftDonw(int index) {
  int element = arr[index];

  // 既然要下滤,说明要有子节点
  // 完全二叉树从上到下,从左到右,遇到第一个叶子结点之后的节点全是叶子结点
  // 所以只要对所有非叶子节点下滤即可
  // 非叶子节点:floor(n/2), 叶子节点:floor((n+1)/2)
  int half = heapSize >> 1;
  while(index < half) {  // 所以 index 必须是非叶子节点

    // 默认 childIndex 是左子节点
    int childIndex = (index << 1) + 1;
    int child = arr[childIndex];

    // 右子节点下标 = 左子节点下标 + 1  
    int rightIndex = childIndex + 1;

    // 右子节点比左子节点大
    if(rightIndex < heapSize && arr[rightIndex] > child) {
      child = arr[rightIndex];
      childIndex = rightIndex;
    }

    // 当前节点比子节点大
    if(element > child) {
      break;
    }

    // 将子节点赋值给当前节点
    arr[index] = child;

    // 更新 index 为较大的子节点下标(左右子节点中较大的那个)
    index = childIndex;
  }
  arr[index] = element;
}

// 2. 重复循环,直至堆中只有一个节点
while(heapSize > 1) {
  heapSize = heapSize - 1;
  // 第一个元素和最后一个元素交换,即把最大的值放到最后
  int tmp = arr[0];
  arr[0] = arr[heapSize];
  arr[heapSize] = tmp;

  // 对第一个元素进行下溢,重新调整堆的结构
  siftDonw(0);
}

6. 快速排序(不稳定)

1.PNG
快速排序的思想如下,找到一个数,使得最终结果,小于这个数的全部在左边,大于这个数的全部在右边,然后对于左边和右边分别进行快速排序,那么,这种把整个数组分成2部分的操作唱叫做 partition ,,具体过程如下示意:
2.PNG
如果arr[i] > v:
3.PNG
但是,如果arr[i] < v:即把arr[i]和arr[j+1]做一个交换,同时j++向后移
4.PNG
经过这样的操作,会得到这样一部分:
5.PNG
最后只需要把l位置和j位置进行交换就能达到我们要的结果:
6.PNG
代码如下:

// 对 arr[l, r] 部分进行 partition 操作
// return index p, make arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template <typename T>
int __partition(T arr[], int l, int r) {
    T v = arr[l];

    // arr[l+1...j] < v; arr[j+1...i) > v
    int j = l;
    for(int i = l + 1; i <= r; i++) {
        if(arr[i] < v) {
            swap(arr[j + 1], arr[i]);
            j++;
        }
    }
    swap(arr[l], arr[j]);
    return j;
}

template <typename T>
void __quickSort(T arr[], int l, int r) {
    if(l >= r) {
        return;
    }
    // p 是 如上 v 的索引
    int p = __partition(arr, l, r);

    // 对左边进行快排
    __quickSort(arr, l, p - 1);

    // 对右边进行快排
    __quickSort(arr, p + 1, r);
}

template <typename T>
void quickSort(T arr[], int n) {
    __quickSort(arr, 0, n - 1);
}

但是,当数组近乎有序或者完全有序的情况,没有找到一个元素小于标定元素,此时,这个快排的递归树高度为 n,时间复杂度退化为 n^2
7.PNG
所以,我们需要随机选择一个元素作为标定元素即可

template <typename T>
int __partition(T arr[], int l, int r) {

    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];

    // arr[l+1...j] < v; arr[j+1...i) > v
    int j = l;
    for(int i = l + 1; i <= r; i++) {
        if(arr[i] < v) {
            swap(arr[j + 1], arr[i]);
            j++;
        }
    }
    swap(arr[l], arr[j]);
    return j;
}

template <typename T>
void __quickSort(T arr[], int l, int r) {
    if(l >= r) {
        return;
    }
    int p = __partition(arr, l, r);
    __quickSort(arr, l, p - 1);
    __quickSort(arr, p + 1, r);
}

template <typename T>
void quickSort(T arr[], int n) {
    srand(time(NULL));
    __quickSort(arr, 0, n - 1);
}

这里已经进行了一步优化,但是,如果存在大量与标定元素相同的元素,会造成分割的左右边非常不平衡,因此我们需要再次优化如下:
8.PNG
当i对应的值>=v, j对应的值<=v时,交换他们的值

此时,我们只需要改变 partition 的逻辑即可(双路快排)

template <typename T>
int __partition2(T arr[], int l, int r) {

    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];

    // arr[l+1...i) <= v, arr[j...r] >= v
    int i = l + 1, j = r;
    while(true) {
        while(i <= r && arr[i] < v) i++;
        while(j >= l + 1 && arr[j] > v) j--;
        if(i > j) break;
        swap(arr[i], arr[j]);
        i++;
        j--;
    }
    // 循环结束后 i 停在从左往右看第一个 >=v 的位置,j 停在从右往左看第一个 <=v 的位置
    // 而标定点的位置在 <= v的这一端,所以要和 arr[j] 交换
    swap(arr[l], arr[j]);
    return j;
}

不过,对于有重复元素的数组,我们除了双路快排,还有3路快排,即把数组分成3部分,小于v的,等于v的,和大于v的
9.PNG
当i=v,直接纳入绿色部分,i++、
当i当i>v,和gt的前一个元素交换即可,gt—

最后处理完成之后的数组就会被处理成这样:
10.PNG
最后,我们只需要把v和lt交换一下即可,然后递归小于v的和大于v的即可,等于v的不用管了,就省了很多元素

template <typename T>
void __quickSort3Ways(T arr[], int l, int r) {
    if(l >= r) {
        return;
    }
    // partition
    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];
    int lt = l; // arr[l+1...lt] < v
    int gt = r + 1; // arr[gt...r] > v
    int i = l + 1; // arr[l+1...i) == v
    while(i < gt) {
        if(arr[i] < v) {
            swap(arr[i], arr[lt + 1]);
            lt++;
        } else if(arr[i] > v) {
            swap(arr[i], arr[gt - 1]);
            gt--;
        } else {
            i++;
        } 
    }
    swap(arr[l], arr[lt]);
    __quickSort3Ways(arr, l, lt -1);
    __quickSort3Ways(arr, gt, r);
}

template <typename T>
void quickSort3Ways(T arr[], int n) {
    srand(time(NULL));
    __quickSort3Ways(arr, 0, n - 1);
}