排序

208 阅读4分钟
🐣 测试用例
#include<iostream>
#include<vector>
using namespace std;

// 省略排序算法...

void output(vector<int>& array) {
    for (int i = 0; i < array.size(); i++) {
        cout << array[i] << " ";
    }
    cout << "\n";
}

int main() {
    vector<int> array = { 49,38,65,97,76,13,27 };
    output(array);
    
    // 这里调用排序算法...
    
    output(array);
    return 0;
}

image.png

插入排序

直接插入排序

  1. 假设 array[0]~array[i-1] 已经排好
  2. 比较 array[i] 与其前面的元素, 边比较边后移元素(即把 array[i] 插入到 array[0]array[i] 之间)
void insertSort(vector<int>& array) {
    int temp;
    // 假设 array[0] 已经排好
    for (int i = 1; i < array.size(); i++) {
        // 依次将 array[1]~array[n] 插入前面已排好序列
        if (array[i] < array[i - 1]) {
            temp = array[i];
            // 从前一位开始后移元素
            int j = i - 1;
            // 直到 array[j] 比 temp 小才停止
            while (j >= 0 && array[j] > temp) {
                array[j + 1] = array[j];
                j--;
            }
            array[j + 1] = temp;
        }
    }
}

空间复杂度:

O(1)O(1)

时间复杂度:

最好的情况下(本来就 有序 , 只进行 n1n-1 次比较):

因为 array[0]~array[i-1] 是有序的, 因此 array[i]array[i-1] 比较一次就可以知道是否需要插入

O(n)O(n)

最坏的情况下(完全逆序, 需进行 n1n - 1 次比较, 移动 i=1n1i\sum_{i=1}^{n-1}i 次)

O((n+2)(n1)2)=O(n2)O\left(\frac{(n+2)(n-1)}{2}\right)=O(n^2)

折半插入排序

  1. 假设 array[0]~array[i-1] 已经排好
  2. 在序列 array[0]~array[i-1] 中使用折半查找
  3. 找到 array[i] 插入位置 high+1
  4. high+1 后面的元素后移
void insertSort(vector<int>& array) {
    int temp, low, high, mid;
    // 假设 array[0] 已经排好
    for (int i = 1; i < array.size(); i++) {
        temp = array[i];

        // 在 array[0] ~ array[i-1] 中折半查找
        low = 0; high = i - 1;
        while (low <= high) {
            mid = (low + high) / 2;
            if (array[mid] > temp) {
                high = mid - 1;
            }
            else {
                low = mid + 1;
            }
        }

        // 找到位置(hegh + 1)后把元素后移一位
        for (int j = i - 1; j >= high + 1; j--) {
            array[j + 1] = array[j];
        }

        array[high + 1] = temp;
    }
}

只能减少找元素位置时的比较次数

O(nlog2n)O(nlog_2n)

不能减少移动次数, 因此时间复杂度:

O(n2)O(n^2)

希尔排序

不稳定排序

按照一定的步长 dk 将数组分成 ⌈n/dk⌉ 组, 并按照步长 dk 进行直接插入排序, 随后逐渐减小 dk , 最终 dk=1, 整个数组变成一组

image.png

void shellSort(vector<int>& array) {
    int temp, len = array.size();
    for (int dk = len / 2; dk >= 1; dk = dk / 2) {
        // 直接插入排序
        for (int i = dk; i < len; i++) {
            // 按步长 dk 进行排序
            if (array[i] < array[i - dk]) {
                temp = array[i];
                int j = i - dk;
                while (j >= 0 && array[j] > temp) {
                    array[j + dk] = array[j];
                    j -= dk;
                }
                array[j + dk] = temp;
            }
        }
    }
}

空间复杂度:

O(1)O(1)
温馨提示☃️

按照理论来说, 应该是下面这种代码, 但是存在 4 层循环, 不够优雅

void shellSort(vector<int>& array) {
    int temp, len = array.size();
    for (int dk = len / 2; dk >= 1; dk = dk / 2) {
        // 共有 dk 列需要直接插入排序
        for(int j = 0; j < dk; j++) {
            // 直接插入排序, 这里步长是 dk
            for (int i = dk + j; i < len; i += dk) {
                // 按步长 dk 进行排序
                if (array[i] < array[i - dk]) {
                    temp = array[i];
                    int j = i - dk;
                    while (j >= 0 && array[j] > temp) {
                        array[j + dk] = array[j];
                        j -= dk;
                    }
                    array[j + dk] = temp;
                }
            }
        }
    }
}

交换排序

冒泡排序

从后往前, 比较相邻的两个数, 如果 array[j] 小于 array[j-1] , 就交换两个数, 一次“冒泡”结束后, 最小的数会移到最左边

image.png

void bubbleSort(vector<int>& array) {
    int len = array.size();
    int temp;
    for (int i = 0; i < len - 1; i++) {
        bool flag = false;
        // 最后一个元素移动到正确位置
        for (int j = len - 1; j > i; j--) {
            if (array[j - 1] > array[j]) {
                // 交换两个数
                temp = array[j];
                array[j] = array[j - 1];
                array[j - 1] = temp;
                flag = true;
            }
        }
        // 如果没有发生交换, 说明已经有序
        if (flag == false) return;
    }
}

空间复杂度:

O(1)O(1)

时间复杂度:

最好的情况下(数组有序, 只比较 n1n-1 次就退出, 不交换):

O(n)O(n)

最坏的情况下(数组逆序):

需要比较 i=1n1(ni)\sum_{i=1}^{n-1}(n-i) 次, 每次交换 3 次, 共交换 i=1n13(ni)\sum_{i=1}^{n-1}3(n-i) 次, 故时间复杂度:

i=1n14(ni)=2n(n1)=O(n2)\sum_{i=1}^{n-1}4(n-i)=2n(n-1)=O(n^2)

平均时间复杂度:

O(n)O(n)

快速排序

以数组中一个数为基准(又称轴点), 将数组分为左右两部分, 左边全部小于基准, 右边全部大于基准, 再对左右两边进行快速排序, 直到左右两边只剩一个数

image.png

分治法(递归实现)

每次递归结束, 能保证一个元素(轴点)处于正确位置

void quickSort(vector<int>& array, int low, int high) {
    if (low < high) {
        int i = low, j = high, 
            x = array[low]; // 基准
        while (i < j) {
            // 从后往前, 大于基准就跳过
            while (i < j && array[j] >= x) {
                j--;
            }
            if (i < j) array[i++] = array[j];

            // 从前往后
            while (i < j && array[i] < x) {
                i++;
            }
            if (i < j) array[j--] = array[i];
        }
        array[i] = x;
        // 递归对左右两部分进行快速排序
        quickSort(array, low, i - 1);
        quickSort(array, i + 1, high);
    }
}
非递归(栈实现)
#include<stack>

// 记录左右子序列位置的类
struct Record {
    int left;
    int right;
    Record(int left, int right) : left(left), right(right) {};
};

void quickSort(vector<int>& array) {
    stack<Record> s;

    s.push(Record(0, array.size() - 1));
    while (!s.empty()) {
        // 获取边界
        // i, j 要变化
        // low, high 不能变, 用于子序列定位
        int i, low;
        i = low = s.top().left;
        int j, high;
        j = high = s.top().right;
        s.pop();

        int x = array[i];
        while (i < j) {
            // 从后往前, 大于基准就跳过
            while (i < j && array[j] >= x) {
                j--;
            }
            if (i < j) array[i++] = array[j];

            // 从前往后
            while (i < j && array[i] < x) {
                i++;
            }
            if (i < j) array[j--] = array[i];
        }
        array[i] = x;

        // 不能越界
        if (i - 1 > low) s.push(Record(low, i - 1));
        if (i + 1 < high) s.push(Record(i + 1, high));
    }
}

空间复杂度:

最好情况

O(log2n)O(log_2n)

最坏情况

O(n)O(n)

时间复杂度:

最好情况

满足递推公式

T(n)=2T(n2)+O(n)T(n)=2T\left(\frac{n}{2}\right)+O(n)

T(n2)T(\frac{n}{2}) 表示每次划分都是对半划分, O(n)O(n) 表示每次都要进行 n1n-1 次比较确定轴点的实际位置

O(nlog2n)O(nlog_2n)

image.png

最坏情况

满足递推公式

T(n)=T(n1)+T(0)+O(n)T(n)=T(n-1)+T(0)+O(n)

若序列本来就有序, 这样扫描全部序列后就会分成一个元素为 0 个一个元素为 n-1 个的两个分组, 同样分组前需要进行 n1n-1 次比较

O(n2)O(n^2)

image.png

选择排序

简单选择排序

从左往右遍历, 每次都找一个最小值与 array[i] 交换

void selectSort(vector<int>& array) {
    int len = array.size(), temp;
    for (int i = 0; i < len - 1; i++) {
        int min = i; // 最小元素位置

        // 往后查找最小元素
        for (int j = i + 1; j < len; j++) {
            if (array[j] < array[min]) min = j;
        }

        // 如果后面存在最小元素, 就交换
        if (min != i) {
            temp = array[i];
            array[i] = array[min];
            array[min] = temp;
        }
    }
}

堆排序

一维数组可以看作一颗二叉树

image.png

初始 i=3, 判断子节点 2 × i + 1 = 7 超过数组范围, 直接跳过

image.png

第二次 i = i - 1 = 2 , 父节点与最大子节点比较, 大于子节点, 直接跳过

image.png

第三次 i=1, 发现小于子节点, 交换

image.png

继续判断子节点的子节点, 即 k=i, i = 2 × i + 1, 发现超过数组范围了, 直接跳过

image.png

第四次 i=0, 小于子节点, 交换

image.png

子节点小于其子节点, 交换

image.png

完成大根堆的构建

从最后一个开始, 与第一个交换, 再重新调整为大根堆, 这样最大值就跑到后面去了

image.png

void headAdjust(vector<int>& array, int k, int len) {
    int i = 2 * k + 1; // 第一个子节点索引
    while (i <= len) {
        // 比较两个子节点, 选最大的
        if (i < len && array[i] < array[i + 1]) {
            i++;
        }

        // 父节点大于子节点, 退出循环
        if (array[k] >= array[i]) break;
        else {
            // 子节点和父节点交换
            int temp = array[k];
            array[k] = array[i];
            array[i] = temp;
            // 继续交换子节点
            k = i;
            i = 2 * i + 1;
        }
    }
}

// 建立大根堆
void buildMaxHeap(vector<int>& array) {
    for (int i = array.size() / 2; i >= 0; i--) {
        headAdjust(array, i, array.size() - 1);
    }
}

// 堆排序
void heapSort(vector<int>& array) {
    buildMaxHeap(array);
    for (int i = array.size() - 1; i > 0; i--) {
        int temp = array[i];
        array[i] = array[0];
        array[0] = temp;
        // 重新建堆
        headAdjust(array, 0, i - 1);
    }
}

时间复杂度

O(nlogn)O(nlogn)

归并排序

image.png

// 合并两个有序数组(这里方法不唯一, 甚至空间复杂度O(1)的也有)
void merge(vector<int>& array, int low, int mid, int high) {
    // 临时数组
    vector<int> temp(array.size());
    for (int k = low; k <= high; k++) {
        temp[k] = array[k];
    }

    // 将数组分成两段
    int i = low, j = mid + 1, k = i;
    for (; i <= mid && j <= high; k++) {
        // 比较临时数组的两段数据, 谁小放入数组中
        if (temp[i] <= temp[j]) {
            array[k] = temp[i++];
        }
        else {
            array[k] = temp[j++];
        }
    }

    // 剩下的没放入的直接放入(因为本来就是有序的)
    while(i <= mid) array[k++] = temp[i++];
    while(j <= high) array[k++] = temp[j++];
}

// 归并排序
void mergeSort(vector<int>& array, int low, int high) {
    if (low < high) {
        int mid = (low + high) / 2;
        // 递归到 low = high + 1 停止(即两两排序)
        mergeSort(array, low, mid);
        mergeSort(array, mid + 1, high);
        merge(array, low, mid, high);
    }
}

非递归版本

void mergeSort(vector<int>& o) {
    for (int step = 2; step < o.size(); step *= 2) {
        for (int i = 0; i < o.size(); i += step) {
            merge(o, i, i + step - 1);
        }
    }

    merge(o, 0, o.size()); // 最后再归并一次
}

时间复杂度

最好最坏均为

O(nlogn)O(nlogn)

与快速排序相比, 归并排序完全满足递推公式

T(n)=2T(n2)+O(n)T(n)=2T\left(\frac{n}{2}\right)+O(n)

空间复杂度

O(n)O(n)