leetcode刷题c++各类排序总结

leetcode刷题c++各类排序总结

写在前面:

对于数据结构和算法来说,排序是最基础的,也是初学时理解较为困难的,这里作以记录,便于我自己复习,也是更加深我自己的理解

1、冒泡排序

冒泡排序几乎是最先了解的排序算法,思想也很简单:
如果每次我们将相邻元素进行对比,并且把较大的元素交换(swap)到后面,那么通过一轮的相邻对比,数组最后的元素就是这个数组中最大的元素。
那么我们进行n轮对比,每次对前n个元素进行相邻比较和交换,那么最终就会得到一个有序的数组。
实现如下 :

void bubble(int *arr,int n) {//冒泡排序
    //每一趟比较后把这一趟找到的最大元素放到数组最后
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n-1; j++) {//注意j<n-1,小心溢出
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1]);
            }
        }
    }
}

这里我们经常会用到swap的动作,初学时一般使用这样的方法进行交换:
假如有两个int类型的变量a和b

    int temp = a;
    a = b;
    b = temp;

所以,可以定义一个swap函数,方便后续的交换:

void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

这里需要使用引用的方式将参数传入函数

int i;
int &j = i;
相当于说“j是i的一个别名”
声明引用的同时必须初始化‘&j = i’ 指向一个已经存在的对象
一旦一个引用被初始化,不能指向其他对象

复杂度:
对冒泡排序来说,平均时间复杂度:O(n2)

2、选择排序

选择算法的思想是:
每次找到这一趟中的最小数,放在数组的开头(位置是走的趟数)
第一趟找到所有n个元素中最小的,放在第一个元素
第二趟找到后n-1个元素中最小的,放在第二个元素
……
第n-1趟找到后两个元素中最小的,放在第n-1元素(倒数第二个)
第n趟找到最后一个元素中最小的,放在第n个元素(最后一个)
实现:

void select(int* arr, int n) {//选择排序
    //每一次找到当前的最小数,放在这次的位置,第一次放在第一个,第二次放在第二个……
    for (int i = 0; i < n; i++) {
        int min = i;
        for (int j = i; j < n; j++) {
            if (arr[j] < arr[min]) {
                min = j;
            }
        }
        if (min != i) {
            swap(arr[i], arr[min]);
        }
    }
}

复杂度:
对选择排序来说,平均时间复杂度:O(n2)

3、插入排序

插入排序的思想,类似扑克牌整理:
假设前m个元素已经有序,对第m+1个元素,向前比较,直到找到新插入的元素(扑克牌)的位置
继续对第m+2个元素插入
直到第n个元素完成插入,数组有序

void insert(int* arr, int n) {//插入排序(扑克牌洗牌
    //在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
    for (int i = 1; i < n; i++) {
        while (i-1>=0&&arr[i]<arr[i-1])
        {
            swap(arr[i], arr[i - 1]);
            i--;
        }
    }

}

说明:这里i-1>=0,即为i>=1,也就是说,插入排序每次比较的时候,都是以1为gap进行比较的,这里的理解对完成 希尔排序 有很大帮助

复杂度:
对插入排序来说,平均时间复杂度:O(n2)

4、希尔排序

思想:
在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。
然后逐渐将增量减小,并重复上述过程。直至增量为1,此时数据序列基本有序,最后进行插入排序。

实现:

void shell(int *arr,int n) {//如果数据序列基本有序,使用插入排序会更加高效。
    for(int gap=n/2;gap>0;gap=gap/2){
        for (int i = gap; i < n; i++) {
            while (i-gap >=0 && arr[i] < arr[i - gap]) {
                swap(arr[i], arr[i - gap]);
                i = i - gap;
            }
        }
    }
}

说明:
对希尔排序的理解,可以理解为,每次插入排序的时候,gap不为1。这就意味着,其实希尔排序是分组按照gap规定的组进行插入排序的
每次 gap /= 2(gap=gap/2),最终就是当gap为1时,其实就是在插入排序,所以说希尔排序是在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。

复杂度:
对希尔排序来说,平均时间复杂度:希尔排序时间复杂度是 O(n^(1.3-2))

5、归并排序

思想:
merge函数用于将两个有序的数组合并为一个数组
实现:只要从比较2个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。

void merge(int a[], int temp[], int l, int r, int mid) {
    int i = l;//左半区第一次没排序元素
    int j = mid+1;//右半区第一次没排序元素
    int t = l;//temp临时数组下标

    while (i <= mid && i <= r) {
        if (a[i] <= a[j]) {
            temp[t] = a[i];
            t++;
            i++;
        }
        else {
            temp[t] = a[j];
            t++;
            j++;
        }
    }
    while (i <= mid) {
        temp[t] = a[i];
        t++;
        i++;
    }
    while (j <= r) {
        temp[t] = a[j];
        t++;
        j++;
    }

    while (l<=r) {
        a[l] = temp[l];
        l++;
    }
}

归并排序入口:

void msort(int *arr,int *temp,int l,int r) {
    if (l < r) {
        int mid = (l + r) / 2;
        //左半区递归
        msort(arr, temp, l, mid);
        //右半区递归
        msort(arr, temp, mid + 1, r);
        //合并函数
        merge(arr,temp,l,r,mid);
    }
}
void Merge(int* arr, int n) {
    int* temp = new int[n];
    for (int i = 0; i < n; i++) {
        temp[i] = 0;
    }
    //show(temp, n);
    msort(arr, temp, 0, n - 1);
    //show(temp,n);
    
};

复杂度:
对归并排序来说,平均时间复杂度:平均时间复杂度:O(NlogN)
计算:设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。

6、堆排序

思想:
利用堆的性质:

大顶堆,子孩子都小于父节点
对下标为i的节点,父节点为(i-1)/2,左孩子节点i2+1,右孩子节点i2+2
一般用数组存储顶堆
小顶堆,父节点小于等于孩子节点
数组存储,大顶堆顺序大到小,小顶堆顺序小到大

因为大顶堆的堆顶是最大的数,每次把它取出,维护大顶堆,让堆顶再次成为最大的数,反复这个过程,就能实现排序。
实现:
大顶堆维护:

void heapify(int arr[], int len, int i)
{
    int largest = i;
    int lson = i * 2 + 1;
    int rson = i * 2 + 2;

    if (lson < len && arr[largest] < arr[lson])
        largest = lson;
    if (rson < len && arr[largest] < arr[rson])
        largest = rson;
    if (largest != i)
    {
        swap(arr[largest], arr[i]);
        heapify(arr, len, largest);
    }
}

堆排序入口:

// 堆排序入口
void heap_sort(int arr[], int len)
{
    int i;
    // 建堆
    for (i = len / 2 - 1; i >= 0; i--)//i=(len-2)/2//其实是假设最后一个数为右子结点求出的父节点下标
    {
        heapify(arr, len, i);
    }

    // 排序
    //建堆完成之后,相当于最大值在堆顶,每次把最大值取出,然后重新排序剩下的堆
    for (i = len - 1; i > 0; i--)
    {
        swap(arr[i], arr[0]);
        heapify(arr, i, 0);//这里的i就是len-1,其实就是新的len(新的数组长度
    }
}

平均时间复杂度:O(NlogN)
由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。

7、快速排序

思想:
基本思想:(分治)

先从数列中取出一个数作为key值;
将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
对左右两个小数列重复第二步,直至各区间只有1个数。

可以这样理解:
挖坑填数

数组:72 - 6 - 57 - 88 - 60 - 42 - 83 - 73 - 48 - 85

初始时 i = 0; j = 9; key=72
由于已经将a[0]中的数保存到key中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。
从j开始向前找一个比key小的数。当j=8,符合条件,a[0] = a[8] ; i++ ; 将a[8]挖出再填到上一个坑a[0]中。
这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。
这次从i开始向后找一个大于key的数,当i=3,符合条件,a[8] = a[3] ; j-- ; 将a[3]挖出再填到上一个坑中。
数组:72 - 6 - 57 - 88 - 60 - 42 - 83 - 73 - 48 - 85

此时 i = 3; j = 7; key=72 再重复上面的步骤,先从后向前找,再从前向后找。
从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
从i开始向后找,当i=5时,由于i==j退出。 此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将key填入a[5]。
数组:48 - 6 - 57 - 88 - 60 - 42 - 83 - 73 - 88 - 85

可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
数组:48 - 6 - 57 - 42 - 60 - 72 - 83 - 73 - 88 - 85

实现:

void q_sort(int *arr, int l, int r) {//left和right两个指针
        //先从数列中取出一个数作为key值;
        //将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
        //对左右两个小数列重复第二步,直至各区间只有1个数。
    if (l > r)
        return;
    int i = l;
    int j = r;
    int val = arr[l];
    while(i<j) {
        while (arr[j] >= val&&i<j) {//从右向左找到一个小于val的值
            j--;
        }
        if (i < j) {
            arr[i]=arr[j];//小于val的值与val交换
            i++;
        }
        while (arr[i] <= val && i < j) {//从左向右找到一个大于新的val的值
            i++;
        }
        if (i < j) {
            arr[j] = arr[i];//大于val的值与新val值交换
        }
    }
    arr[i] = val;//最终找到i使前面的都小于val,后面的都大于val
    //递归对左右部分递归
    q_sort(arr,l,i-1);
    q_sort(arr,i+1,r);
}

函数入口:

void quick(int *arr,int n) {

    q_sort(arr,0,n-1);
}

复杂度:
平均时间复杂度:O(N*logN)

排序算法选择总结:常用快速排序,需要考虑空间复杂度时使用堆排序,需要考虑稳定性时使用归并排序。

以上,就是我个人的一些记录,在我看来,真理无穷,无限进步吧!

我是栗子,祝你幸福。


版权声明:本文为i_choose_game原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。