常见的排序算法

写一写常见的排序算法的思想,包括 冒泡排序、选择排序、插入排序、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(稳定).

原文地址:https://www.cnblogs.com/daihanlong/p/5452782.html