• 常见的排序算法


    写一写常见的排序算法的思想,包括 冒泡排序、选择排序、插入排序、shell排序、归并排序、快速排序、堆排序

    冒泡排序

    • 思路:通过不停的交换相邻元素.每一次循环都把最后未排序的元素里面最大的移动到后面去了.每次循环后, 未排序的元素就会减少1.
    // 通过交换相邻的两个元素达到排序的目的
    // 每次循环相当于把最大的一个放到后面
    void my_maopao(int* arr,int len){
    
        if (len < 2) {
            return;
        }
    
        // 外层循环相当于控制次数
        for (int i = 0; i < len - 1; ++i) {
    
            // 内层循环是不停的比较交换相邻的两个数
            for (int j = 0; j < len - 1 - i; ++j) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
    }
    

    简单验证下正确性:

    void test_rank(){
    
        for (int i = 0; i < 100; ++i) {
    
            // 产生2组数据
            vector<int> arr1;
            vector<int> arr2;
            int count = rand() % 1000 + 1000;
            for (int j = 0; j < count; ++j) {
                int num = rand() % count;
                arr1.push_back(num);
                arr2.push_back(num);
            }
    
            // 用我们实现的算法对 arr1 进行排序
            // 用 stl 的 sort 对 arr2 进行排序
            // 然后验证结果是否正确
            my_maopao(arr1.data(),(int)arr1.size());
            sort(arr2.begin(),arr2.end());
            for (int j = 0; j < arr1.size(); ++j) {
                assert(arr1[j] == arr2[j]);
            }
        }
    
        printf("测试通过.
    ");
    }
    

    选择排序

    • 思路: 每次都从未排序的数组中选择一个最大的,然后放到合适的位置.
    // Q:其实这是选择排序
    // 思想就是,每次从未排序的里面选择一个最大的 放到后面
    void my_select_sort(int* arr,int len){
    
        if (len < 2) {
            return;
        }
    
        // 升序排列
        // 外面的循环控制次数
        for (int i = 0; i < len - 1; ++i) {
            int maxIndex = 0;
    
            // 内循环每次找出最大的值,然后放到合适的位置
            for (int j = 0; j < len - i; ++j) {
                if (arr[j] > arr[maxIndex]) {
                    maxIndex = j;
                }
            }
            // 将 maxIndex 放到正确的位置
            if (maxIndex != len - i - 1) {
                int temp = arr[maxIndex];
                arr[maxIndex] = arr[len - i - 1];
                arr[len - i - 1] = temp;
            }
        }
    }
    

    验证方法同上.

    插入排序

    • 思想: 每次拿一个数字出来,插入到前面已经排序的序列中.
    // 插入排序
    void my_insert_sort(int* arr,int len){
        if (len < 2) {
            return;
        }
    
        // 从索引为1开始,依次插入
        // 表示 i 前面的是已经排好序的
        for (int i = 1; i < len; ++i) {
    
            // 应该是从后往前比较
            // 从已经排好序的里面找一个比 arr[i]要小的,然后放到其后面
            // 如果没有找到,则放到索引为0的位置
            for (int j = i-1; j >=0; --j) {
                if (arr[j] > arr[ j + 1]) {
                    // 交换
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }else{
                    continue;
                }
            }
        }
    };
    

    shell排序

    • 思想:先按照一个增量(间隔)进行分组,然后对每个分组进行插入排序,然后减小增量,继续对分组进行插入排序. 要求增量一定能减小到1,进行一次插入排序.
    • 和插入排序的区别: 当需要排序的数组比较大时,插入排序时,可能会需要进行比较大量的移动.shell排序进行分组排序的目的就是减少插入时的移动.
    void my_shell_sort(int* arr,int len){
        if (len < 2) {
            return;
        }
    
        // 选取合适的增量,需要保证 N 可以缩减到 N = 1
        // 一般取质数,N = len
        // 我们这里取 N = N/3 + 1
        // 假如len = 20,则开始 N = 7
        // 分组情况为:
        //   a[0],a[7],a[14]
        //   a[1],a[8],a[15]
        //   a[2],a[9],a[16]
        //   a[3],a[10],a[17]
        //   a[4],a[11],a[18]
        //   a[5],a[12],a[19]
        //   a[6],a[13]
        //  第一次循环就是分别对这些分组进行插入排序
    
        int N = len;
    
        // 让增量N缩减到1
        while (N > 1) {
            N = N / 13 + 1;
    
            // 对分组进行循环,对 a[N]到a[len-1] 进行分别插入
            for (int i = N; i < len; ++i) {
    
                // j 表示各个分组里面的前一个元素,各个分组前面的元素都是已经排好序的
                // j -= N 表示 a[j]所在分组的前一个元素
                for (int j = i - N; j >= 0; j -= N) {
                    if (arr[j] > arr[j + N]) {
                        // 交换
                        int temp = arr[j];
                        arr[j] = arr[j + N];
                        arr[j + N] = temp;
                    }else{
                        continue;
                    }
                }
            }
        }
    }
    

    说明 N = N/13 + 1,选择增量最好是选择质数.

    归并排序

    • 思想:将数组分为左右两部分,对两部分进行归并排序,最后合并.分治思想的典型例子.
    // 将两个已经排序的数组合并起来
    // start <= mid <= end
    // 表示 arr[start,mid] arr[mid+1,end] 都已经排好序
    void my_meger_1(int* arr,int start,int mid,int end){
    
        if (start >= end) {
            return;
        }
    
        int lenL = mid - start + 1;
        int lenR = end - (mid + 1) + 1;
    
        int L[lenL];
        int R[lenR];
    
        for (int i = 0, j = start; i < lenL; ++i,++j) {
            L[i] = arr[j];
        }
        for (int i = 0, j = mid + 1; i < lenR ; ++i,++j) {
            R[i] = arr[j];
        }
    
        // 一个一个的放入数组
        int i = 0;
        int j = 0;
        int idx = start;
        while (i < lenL && j < lenR) {
            if (L[i] > R[j]) {
                arr[idx] = R[j];
                ++j;
                ++idx;
            }else{
                arr[idx] = L[i];
                ++i;
                ++idx;
            }
        }
    
        // 有可能有一边还没处理
        if (i == lenL) {
            for (; j < lenR; ++j) {
                arr[idx] = R[j];
                ++idx;
            }
        }else{
            for (; i < lenL; ++i) {
                arr[idx] = L[i];
                ++idx;
            }
        }
    }
    
    // 对 arr 的 [start,end]
    void recursive_meger_sort_1(int* arr,int start,int end){
    
        if (start == end) {
            return;
        }
    
        int mid = (start + end) / 2;
        recursive_meger_sort_1(arr,start,mid);
        recursive_meger_sort_1(arr,mid + 1, end);
        my_meger_1(arr,start,mid,end);
    }
    
    // 归并排序
    // 这里使用,先对数组进行递归分组
    void my_meger_sort_1(int* arr,int len){
        if (len < 2) {
            return;
        }
    
        recursive_meger_sort_1(arr,0,len - 1);
    }
    

    快速排序

    • 思想: 和归并类似,也是分治思想. 从数组中选择一个元素A,根据此元素将数组分成左右两部分,左边都小于A,右边都大于等于A,然后对左右进行递归.
    int my_partition(int* arr,int start,int end){
        // 这里我们简单的取 arr 的第一个元素作为分割的标准数
        int key = arr[start];
    
        // arr[start,end]范围内比 key 小的都放在左边,比 key 大的都放在右边
        // 可以这样思考,有几个比 key 小,则 key 的索引就是几
        int len = end - start + 1;
        int keyIndex = start;
        for (int i = 1; i < len; ++i) {
            if (arr[start + i] < key) {
                // 需要将 arr[start + i]移动到key当前位置的前面
                int temp = arr[start + i];
                for (int j = start + i - 1; j >= keyIndex; --j) {
                    arr[j + 1] = arr[j];
                }
                arr[keyIndex] = temp;
                ++keyIndex;
            }
        }
        return keyIndex;
    }
    
    void my_quick_recursion(int* arr,int start,int end){
    
        // 递归的结束条件
        if (start >= end) {
            return;
        }
    
        int index = my_partition(arr,start,end);
        my_quick_recursion(arr,start,index - 1);
        my_quick_recursion(arr,index + 1,end);
    }
    
    // 快速排序
    void my_quick_sort(int* arr,int len){
        if (len < 2) {
            return;
        }
    
        my_quick_recursion(arr,0,len-1);
    }
    

    堆排序

    • 思想:利用二叉堆数据结构的性质.将数组构建成最大堆,然后不停的从最大堆中获取最大元素放到合适的位置.
      主要要进行几个操作:
      1. 保持最大堆的性质 a[i] >= a[left[i]] && a[i] >= a[right[i]]
      2. 二叉堆是一个完全二叉树,满足完全二叉树的性质.如根节点索引为i,左子树节点 2i + 1,右子树节点 2i + 2.
    //  将 arr idx 索引为根的树保持最大堆的性质
    // 要求 arr[idx]的左子树和右子树都是最大堆
    void keep_heap(int* arr,int idx,int len){
    
        if (idx >= len) {
            return;
        }
    
        int leftIndex = 2 * idx + 1;
        int rightIndex = 2 * idx + 2;
        if (leftIndex >= len) {
            // 左子树不存在,说明是叶子节点
            return;
        }
    
        if (rightIndex >= len) {
            // 右子树不存在,只需要和 a[left[i]] 比较即可
            if (arr[idx] < arr[leftIndex]) {
                int temp = arr[idx];
                arr[idx] = arr[leftIndex];
                arr[leftIndex] = temp;
            }
            return;
        }
    
        // 左子树和右子树都存在
        int largestIndex = idx;
        if (arr[idx] < arr[leftIndex]) {
            largestIndex = leftIndex;
        }
    
        if (arr[rightIndex] > arr[largestIndex]) {
            largestIndex = rightIndex;
        }
    
        if (largestIndex != idx) {
            // 根节点比子节点小,需要将节点 arr[idx] arr[leftIndex] arr[rightIndex] 中最大的提上来
            int temp = arr[idx];
            arr[idx] = arr[largestIndex];
            arr[largestIndex] = temp;
    
            // 原来的 arr[idx] 已经下降,但是下降后,可能比其下降后的左子树/右子树小,导致需要继续下降
            keep_heap(arr,largestIndex,len);
        }
    }
    
    void my_make_heap(int* arr,int len){
        int startIndex = len / 2;
        for (int i = startIndex; i >= 0; --i) {
            keep_heap(arr,i,len);
        }
    }
    
    void my_heap_sort(int* arr,int len){
        if (len < 2) {
            return;
        }
    
        // 1.根据 arr 创建 一个最大堆
        my_make_heap(arr,len);
    
        // 2.进行排序,原理是每次交换根元素和最后一个元素,然后将最后一个元素从堆中排除
        int lastIndex = len - 1;
        while (lastIndex >= 1) {
            int temp = arr[lastIndex];
            arr[lastIndex] = arr[0];
            arr[0] = temp;
            --lastIndex;
    
            keep_heap(arr,0,lastIndex + 1);
        }
    }
    

    总结:

    对于上面的排序,
    冒泡、选择、插入、快速 是稳定的排序.
    shell、归并、快速、堆 是不稳定的排序.
    主要是学习思想,上面可以优化的地方有很多,实际使用还是使用 stl 的 sort(不稳定) 和 stable_sort(稳定).

  • 相关阅读:
    对象,类,封装,继承,多态是什么
    面向对象和非面向对象程序的区别是什么?
    什么是OOA,OOP
    git stash git tag
    从一个远程仓库同步向另一个远程仓库同步代码
    VScode 配置rust开发环境
    IAR ICCARM V8.32.3在Windows Server端进行Ollydbg破解
    阅读笔记
    FP32转FP16能否加速libtorch调用
    阅读笔记-Chained-Tracker
  • 原文地址:https://www.cnblogs.com/daihanlong/p/5452782.html
Copyright © 2020-2023  润新知