首页 > 技术文章 > 十大排序算法速记总结

lh4217 2022-03-01 16:06 原文

1.快速排序 O(nlog(n)) 不稳定排序

  1. 选取第一个数为哨兵

  2. 将比哨兵小的数都交换到前面,比哨兵大的数都交换到后面

  3. 返回哨兵位置,根据哨兵位置划分左右区间重复第二步,直到各区间只有一个数。

int partition(vector<int> &nums, int left, int right){
    int key = nums[left]; // 第一个为哨兵
    int i = left, j = right;

    while(i < j){
        // 要先从右边开始,只要右边的数比哨兵大,就继续
        while(i < j && nums[j] >= key){
            j--;
        }
        // 再找左边,只要左边的数<=哨兵就继续
        while(i < j && nums[i] <= key){
            i++;
        }
        swap(nums[i], nums[j]); // // 走到这步时,一定有nums[i] > nums[left] 而nums[j] < nums[left],为了nums[i] < nums[j] 所以要进行交换
    }
    swap(nums[i], key); // 最后将哨兵放到正确的位置上,完成一次partition
    return i;        // 最后返回哨兵所在位置
}

void QuickSort(vector<int> &nums, int left, int right){
    if(left >= right) return;
    int pivot = patition(nums, left, right);
    QuickSort(nums, 0, pivot - 1);
    QuickSort(nums, pivot + 1, right);
}

2.归并排序 O(nlog(n)) 稳定排序

  1. 将数组划分为左右两个区间,区间内再划分左右区间,直到左右区间中只有一个元素(划分),通过递归实现
  2. 对左右区间元素进行合并, 也就是merge的过程、
  3. merge的思想是设置一个长度为左右两区间长度和的辅助数组,设置两个指针分别从左子区间和右子区间的起始处开始,比较这两个指针指向的元素,谁更小就把谁放到辅助数组中,同时该指针+1。
  4. 当某个区间的指针已经走到头时,跳出循环。将剩余的没有走到头的区间的元素全部搬到辅助数组中。
  5. 将辅助数组中的元素更新到原数组中。完成一次合并排序。
void merge(vector<int> &nums, int left, int mid, int right){
    vector<int> tmp(right - left + 1);  // 设置辅助数组
    int i = left, j = mid + 1, idx = 0;

    while(i <= mid && j <= right){
        if(nums[i] <= nums[j]){
            tmp[idx++] = nums[i++];
        }
        else{
            tmp[idx++] = nums[j++];
        }
    }
    while(i <= mid){
        tmp[idx++] = nums[i++];
    }
    while(j <= right){
        tmp[idx++] = nums[j++];
    }

    for(int i = 0; i < tmp.size(); i++){
        nums[left + i] = tmp[i];
    }
}

void MergeSort(vector<int> &nums, int left, int right){
    if(left >= right) return;
    int mid = left + (right - left) / 2;
    MergeSort(nums, left, mid);
    MergeSort(nums, mid + 1, right);
    merge(nums, left, mid, right);
}

3.堆排序O(nlog(n)) 不稳定排序

思想: 堆是用数组构建的,一个节点i它的左右子节点在数组中的位置分别为2 * i + 1, 2 * i + 2。
首先建立一个堆,然后每次从堆中拿出堆顶元素与数组的最后一个元素交换,再对堆进行调整,循环n次,可以得到排序后的数组。

// 小根堆
void sift(vector<int> &nums, int index, int size){
    int i = index, j = 2 * i + 1;
    while(j < size){
        if(j + 1 < size && nums[j] > nums[j + 1]){  // 比较i的左右孩子,j指向较小者
            j++;
        }
        if(nums[i] < nums[j]) break;
        else{
            swap(nums[i], nums[j]);
            i = j; j = 2 * i + 1;   // i指向交换后的新位置,继续向下比较,一直下沉到叶子节点
        }
    }
}

void HeapSort(vector<int> &nums){
    int n = nums,size();
    // 初始建堆, 从最后一个非叶子结点开始
    for(int i = n / 2 - 1; i < n; i++){
        sift(nums, i, n);
    }
    // 排序,重复移走堆顶元素,再调整堆
    for(int i = 0; i < n; i++){
        swap(nums[0], nums[n - i - 1]);
        sift(nums, i, n - i - 1);
    }
}

4.冒泡排序(O(n^2) 稳定排序

冒泡排序就是把小(或大)的元素往前调,比较的是相邻两个元素,交换也发生在这两个元素之间。

  1. 比较相邻的元素,如果第二个比第一个大,就交换他们两个。
  2. 对每一对相邻的元素执行步骤一,从开始到结尾,最后一个元素会是最大的数。
  3. 每次对越来越少的元素重复上面的步骤
void BubbleSort(vector<int> &nums){
    for(int i = 0; i < nums.size(); i++){
        for(int j = 0; j < nums.size() - i - 1; j++){
            if(nums[j] < nums[j + 1]){
                swap(nums[j], nums[j]);
            }
        }
    }
}

5.简单插入排序(O(n^2)) 稳定

每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止。

初始有序序列为数组第一个元素


void InsertSort(vector<int> &nums){
    int n = nums.size();
    for(int i = 1; i < n; i++){
        int key = nums[i];               // 待插入的数据key
        for(int j = i - 1; j >= 0; j--){
            if(key >= nums[j]){         
                break;
            }
            nums[j + 1] = nums[j];
        }
        nums[j + 1] = key;
    }
}

6.希尔排序(O(n^2 - nlog(2n)) 不稳定)

希尔排序是对直接插入排序的改进,采用插入排序的方法,先让数组中任意间隔为h的元素有序,刚开始h=n/2,接着让h=n/4,让h一直缩小,当h=1时,也就是此时数组中任意间隔为1的元素有序。
排序完成。

void ShellSort(vector<int>& nums){
    int n = nums.size();
    for(int dis = n / 2; dis >= 1; dis /= 2){
        for(int i = dis; i < n; i++){
            int key = nums[i];
            for(int j = i - dis; j >=0; j--){
                if(key >= nums[j])
                    break;
                nums[j + dis] = nums[j];
            }
            nums[j + dis] = key;
        }
    }
}

7.选择排序(O(n^2) 不稳定)

思想:给每个位置选择当前元素最小的

  1. 在未排序的数组中找到最大(小)元素,存放到排序序列的起始位置
  2. 从剩余未排序的元素中继续找最大(小)元素放到已排序序列的末尾
  3. 重复直到所有元素均排序

void SelectSort(vector<int> &nums){
    int n = nums.size();
    int maxIndex = 0;
    for(int i = 0; i < n; i++){
        maxIndex = i;
        for(int j = i + 1; j <n; j++){
            if(nums[j] > nums[maxIndex]){
                maxIndex = j;
            }
        }
        swap(nums[i], nums[maxIndex]);
    }
}

8.计数排序(O(n + k) 稳定)

思想:利用额外的数组空间,但是元素需要在0~k之间,不能小于0

  1. 首先找到待排序数组中最大和最小的元素,以最大元素值为长度构建辅助数组
  2. 统计待排序数组中每个值出现的次数,以值为下标,次数为值放入辅助数组中
  3. 遍历辅助数组,从中按顺序拿出数组值(出现次数)不为0的下标,每拿一次,次数减一,直到为0
  4. 拿出所有后排序完成。

本质是将值转化为数组索引进行排序。

9.桶排序

将数组分到有限数量的桶里,每个非空的桶再分别排序,从不是空的桶子里把元素再拿出来放回原来序列中。

10.基数排序,O(k * n) 稳定

也是一种桶排序,将整数按位数切分为不同的数字(个位, 十位,百位...),从最低为开始,依次进行桶排序。
所有位数字排序完成后,排序完成

总结:
计数排序,桶排序和基数排序都用到了同的概念,

  • 计数排序:每个桶只存储单一键值。
  • 桶排序:每个桶存储一定范围的数值
  • 基数排序:根据键值的每位数字来分配桶

推荐阅读