小白学C语言算法之排序

前言

排序方式是真的很多...

学校里只接触了冒泡,还觉得冒泡挺难...

学了之后感觉还有好多好难得排序...

基于比较的排序

时间复杂度O(n^2), 空间复杂度O(1)的算法

感觉这种算法大多的特点都是 需要遍历 且遍历中还需要遍历...

时间复杂度太高,不适合处理大批量数据

选择排序

思路:每一次从待排序的数据元素中选取最小(或最大)的一个元素作为首元素,直到所有元素排序完成。

比如 这样, 7 5 9 2 8

第一次循环,找出最小值2,让2位于0位置

第二次循环,找出次小值5,让5位于1位置...

保证数组中,每个最小的数字都在数组的最左侧

void selectSort(int arr[], int n){
    if(arr ==NULL){
        printf("Error: the arr is empty!");
        return;
    }
    int i,j, min;
    for(i=0; i< n-1; i++){            //外层循环用于遍历数组
        min = i;
        for(j= i+1; j<n; j++){        //内层循环用于找到最小元素
            if(arr[j] <arr[min]){     // 如果当前遍历到的元素小于最小值 就把二者的下标交换
                min = j;
            }
        int temp = arr[min];          // 交换值
        arr[min] = arr[i];
        arr[i] = temp;
    }
}

冒泡排序

冒泡排序的思路就是

遍历数组

相邻的两个元素比较, 如果前者大于后者,就换位置

比如

7 5 9 2 8

第一轮换 5 7 9 2 8 -》5 7 2 9 8-》 5 7 2 8 9

这时可以保证,数组最右边的数字一定是整个数组中最大的值,下次只需要从 0位置遍历到 n-1位置就可以

void bubSort(int arr[], int n){
    if(arr ==NULL){
        printf("Error: the arr is empty!");
        return;
    }
    int i, j;
    for(i= 0; i< n-1; i++){                  // 相当于设定遍历多少次
        for(j = 0; j<n-i-1; j++){            // 整个数组遍历并交换
            if(arr[j] > arr[j+1]){
                int temp = arr[j];            
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}
    

插入排序

思路:

将未排序的元素插入到已经有序的序列中去。

比如

7 5 9 2 8

在第一轮循环中,只有7被看作有序部分, 5 9 2 8无序

在第二轮循环中,无序中第一个数字变成有序的 5 7部分,9 2 8仍无序

第三轮中,5 7 9...

void insertSort(int arr[], int n){
    if(arr == NULL){
        printf("Error: the arr is empty!");
        return;
    }
    
    int i, j, key;
    for(i=1; i<n; i++){
        key = arr[i];
        j = i-1;
        while(j >=0 && arr[j] >key){
            arr[j+1] = arr[j];
            j--;
        }
    }
}

时间复杂度O(n logn),空间复杂度O(logn)的算法

快速排序

常见的不基于比较的排序中只有一个符合这个要求:快速排序

快速排序前的partition

研究快速排序前,要先研究partition

partition的思路是这样的:

有这样一组数:7 5 9 2 8

想实现这样一件事:小于7的所有值放在7的左侧,大于7的放在7的右侧

实现思路:先设定一个小于等于区,右边界设为 k=- 1

a[0]先和num比较

if a[i]> num{

i++;} a[0]比num大,直接跳过

a[0]比num小 让a[i]和小于等于区的最近的一个数 a[k+1] 再让右边界 k++

跳到越界就停

这样排下来就实现了遍历一遍

基本上是双指针的思想

void partition(int arr[], int n, int num){
    if(arr == NULL){
        printf("Error: the arr is empty!");
        return;
    }
    
    int k=-1, i=0;
    while(i < n){                // 遍历整个数组
        if(arr[i] <= num){        // 如果arr[i]小于设定的num值
            k++;
            int temp = arr[i];    //二者做值的交换
            arr[i] = arr[k];
            arr[k] = temp;
            
        }                         //如果arr[i]大于num,不需要操作
        i++;                      //继续往下遍历
    }
}
partition实现的荷兰国旗划分问题

荷兰国旗划分问题,简单地说就是将一个数组划分成三个部分:

小于区、等于区、大于区

7 5 9 2 8中, 如果设定num =7

可能实现出的效果就是

5 2 | 7 | 8 9

A B C

这样三个区

实现思路

设置i记录数组位置, 设置l做左边界值 r做右边界值

1.如果a[i]== num, 应该分配在中间区域 就直接i++;

2.如果a[i]<num , 和小于等于区最右边的数做交换 a[i] = a[l+1] 然后l++;

3.如果a[i]>num, a[i] = a[r-1], 但是i不动(因为i还没遍历到r-1位置, 所以需要再验证一次)

i和r位置重合 循环停止

void helanguoqi(int arr[], int n, int num){
    if(arr == NULL){
        printf("Error: the arr is empty!");
        return;
    }
    int i =0, left = -1, right = n-1;
    while(i < right){
        if(arr[i] ==num){        
            left++; 
            int temp = arr[i];
            arr[i] = arr[left];
            arr[left] = temp;
            i++;
        }
        else if(arr[i] > num){
            int temp = arr[i];
            arr[i] = arr[right];
            arr[right] = temp;
            right--;
        }
        else{ i++;
        }
    }    
}

由上述partition思想引发的排序方法:快速排序

还是用7 5 9 2 8举例

经过partition后,假设num设成了7

5 2 | 7 | 8 9

现在分成了三个区域

5 2是小于num区

7是等于num区

8 9是大于num区

那如果把 5 2看成一个整体 , 7看成一个整体 ,8 9看成一个整体,其实整个数组已经是有序的了

(5,2)<(7)<(8, 9)

但是现在再拆分来看,5,2 这明显是仍然无序的。而且8, 9 也可能是9, 8,不一定有序。

重新把这三个分区都看成一个新的整体来做partition。

5,2,把2当作num来partition

2的小于区是null, 等于区是2, 大于区是5

再这样依次递归....就能实现序列有序

但是有一个问题,假设第一次我选择的数字不是7呢?

就是说,我在两眼一摸黑的时候,或是在十万个数字我看都看不过来的时候,我怎么能这么精确的选中一个中数?显然不可能。

而且有一种极端情况:我每次选的值都是最大值或是最小值,那这种快排就会变得像前面几种排序一样,时间复杂度O(n^2)。

在这种顾虑下,优化产生了:随机快排。

为什么随机

当然是尽量防止每次选到数组中的最值,递归那么多次,每次的num(分界值)都随机,如果这还每次都选到最值的话,建议出门买彩票。

coding!

//先把上面的荷兰国旗分法复制粘贴下来
void partition2(int arr[], int n, int num){
    if(arr == NULL){                    
        printf("Error: the arr is empty!");
        return 0;
    }
    int i=0, left = -1, right = n-1;
    while(i < right){
        if( arr[i] == num){
            left++;
            int temp = arr[i];
            arr[i] = arr[left];
            arr[left] = temp;
            i++;
        }
        else if(arr[i] > num){
            int temp = arr[i];
            arr[i] = arr[right];
            arr[right] = temp;
            right--;
        }
        else{
            i++;
        }
    }
}
void quickSort(int arr[], int n){
    if(n <= 1){                    
        printf("Error: the arr is empty!");
        return;
    }
    srand(time(NULL));          //设置随机种子数
    int num = arr[rand() % n];          // num为数组中的随机数
    partition2(arr, n, num);

    int i;
    for(i = 0; i < n; i++){
        if(arr[i] == num){
            break; // 找到基准数的索引
        }
    }
    quickSort(arr, i); // 递归地对左侧子数组进行排序
    quickSort(arr+i+1, n-i-1); // 递归地对右侧子数组进行排序
}

时间复杂度O(n logn), 空间复杂度 O(n)的算法

归并排序

归并排序的特点是:无穷无尽的递归

归并本身的代码并不复杂

甚至说看见了以为写错了..

void mergeSort(int arr[], int L, int R){  //L是左,R是右
    if(L== R){
        return;
    }
    int mid= L+ ((R- L) >>1);            // (R-L)>>1是右移一位,二进制中相当于除以二
    mergeSort(arr, L, mid);                    // 看起来很明确  左半边排序
    mergeSort(arr, mid+1, R);                  // 右半边排序
    merge(arr, L, mid, R);                // 合并
}

Sort本身不难,关键在于merge怎么做

比如这样

1 3 5 7 9 | 0 2 4 6 8

两边都已经有序了,怎么merge才能merge出0 1 2 3 4 5 6 7 8 9?

归并思路

思路大概是这样

1 2 3 5 | 2 4 6

^ ^

设置help数组 用于copy

1.开始时,左右两侧的指针都指在最左侧

而help[0]位置, copy较小的那个数字

所以此时help = [0]

2.左边指针此时右移

1 2 3 5 | 2 4 6

^ ^

这时候,两边相等,优先copy左侧的2 这时指针再次右移...

3. 重复,直到某一侧的指针越界

coding!

这样就能实现把两个有序的数组变成一个有序的数组

代码
void merge(int arr[], int L, int mid, int R){
    // 这里面的L是左边界,mid是中点,R是右边界
    int* help = (int*)malloc((R-L+1)* sizeof(int));  //开一个辅助数组
    int p1= L, p2= mid+1;   //定义一下左右指针分别在哪
    while(p1< mid && p2< R){   //两侧都没越界时
        // 哪边小就copy哪边 而且相等的话优先copy左边
        help[i++] = arr[p1] <= arr[p2] ? arr[p1++]: arr[p2++]
    }
    while(p1 <=mid){         //这边的潜台词就是,右侧已经越界了,那直接全copy走就可以
        help[i++] = arr[p1++];
    }
    while(p2 <= R){
        help[i++] = arr[p2++];
    }
    for(i= 0; i< R-L+1; i++){     //把help数组倒回原数组
        arr[L+ i] = help[i]; 
    }
    free(help);
}  

时间复杂度O(n logn), 空间复杂度 O(1)的算法

常见的基于比较的排序中,符合条件的有希尔排序和堆排序(希尔排序不会,我直接开摆

堆排序

在了解堆排序之前,肯定要先了解什么叫“堆”。

在了解堆之前,就得了解什么是完全二叉树

完全二叉树

完全二叉树,我的理解就是那些 “ 一定满足头、左、右这个顺序优先‘存在’的二叉树”

举个栗子

1

2 3

4 5 6

这就是完全二叉树,因为满足这个次序

而如果变成

1

2 3

4 5 6

3有右树却没有左树,这就很明显不是完全二叉树。

堆, 一定是完全二叉树

大根堆和小根堆

何为大根堆,何为小根堆

大根堆顾名思义:根一定比子树大的堆

上面的

1

2 3

4 5 6

就需要变成

6

5 3

4 2 1

那具体操作思路呢?

从最下层节点开始,向上遍历。应该是先左后右的顺序(因为如果存在右一定存在左,存在左不一定存在右)

那先写一个堆的交换函数,然后递归的方式往上遍历

coding!

void heapChange(int arr[], int i, int limit){
    if(arr == NULL){                    
        printf("Error: the arr is empty!");
        return;
    }
    int temp;
    int largest = i;                // 从0开始时,子树和父节点的下标关系
    int left = 2*i +1;                   
    int right = 2 * i +2;
    
    if(left <limit && arr[left]> arr[largest]){  //如果左边子树没越界 且左子树大于父树
        largest = left;                          // 下标交换 此时left是最大那棵树
    }              
    if(right< limit && arr[right] > arr[largest]){
        largest = right;
    }
    if(largest != i){                            // 如果largest不是原本那个父树了
        temp = arr[i];                            // 交换“原父树”和新的largest
        arr[i] = arr[largest];                    // 让新的largest变成新父树
        arr[largest] = temp;
        heapchange(arr, largest, limit);
     }
}
void bigHeap(int arr[], int limit){
    int i;
    for(i = limit/2 - 1; i>=0; i--){        //从下往上遍历,时间复杂度更低
        heapchange(arr, i, limit);
    }
}
小根堆

小根堆几乎等于大根堆,只需要调整一些位置,代码在下面的堆排序中实现吧~

堆排序

大根堆实现了父树永远大于子树,这在某种意义上来说,也是一种排序(父>子)

在无穷无尽的递归下,也可以实现排序效果,coding!

void heapChange(int arr[], int i, int limit){   //小根堆的交换法则
    if(arr == NULL){                            
        printf("Error: the arr is empty!");
        return;
    }
    
    int min = i, left = 2*i +1, right = 2*i +2;  //先定义好各个位置
    if(left<lmit && arr[left]< arr[min]){
        left = min;
    }
    if(right < limit && arr[right] < arr[min]{
        right = min;
    }
    if(min != i){
        int temp = arr[i];                            // 交换“原父树”和新的largest
        arr[i] = arr[min];                    // 让新的largest变成新父树
        arr[min] = temp;
        heapchange(arr, min, limit);
     }
}

void minHeap(int arr[], int limit){          // 造小根堆的函数
    int i;
    for(i = limit/2 - 1; i>=0; i--){        //从下往上遍历,时间复杂度更低
        heapchange(arr, i, limit);
    }
}

void heapSort(int arr[], int n) {     //堆排序
    int i;
    minHeap(arr, n); // 先将数组建成小根堆
    for (i = n - 1; i > 0; i--) {
        int temp = arr[0];        // 将堆顶元素与最后一个元素交换位置
        arr[0] = arr[i];
        arr[i] = temp;
        heapChange(arr, 0, i);   // 对交换后的堆顶元素进行维护
    }
}
    

deal!

不基于比较的排序

不基于比较的排序有一个共同特点:都对数据本身有严格要求。但是同时,他们的时间复杂度只有O(n+k)

计数排序

比如一家公司的员工,年龄应该都是0-200岁/手动狗头

操作是:

建立一个help数组(大小为 200-0+1)

这时候我的help数组是由201个0构成的

开始遍历,如果第一个是1岁的员工 就在arr[1]位置++

此时help数组变成 0100000000000......

遍历之后,比如数组前几位是 3 2 5 1 2

对应位置分别是 0 1 2 3 4

排序就排出来了: 000 11 22222 3 44.......and so on

coding也很简单

void countSort(int arr[], int n, int min, int max){// min和max为了让数据有个输入范围
    int* help = (int*)malloc((max-min+1) * sizeof(int));
    for(int i= 0; i< n; i++){
        int temp = arr[i]- min;
        help[temp]++;
    }
    // 累加计数
    for (int i = 1; i <= max-min; i++) {
        help[i] += help[i-1];
    }

    // 排序到输出数组中
    int* output = (int*) malloc(n * sizeof(int));
    for (int i = n-1; i >= 0; i--) {
        int temp = arr[i] - min;
        output[help[temp]-1] = arr[i];
        help[temp]--;
    }

    // 将排序好的数组复制到原始数组中
    for (int i = 0; i < n; i++) {
        arr[i] = output[i];
    }

    // 释放内存
    free(help);
    free(output);
} 

基数排序

需要数据十进制非负

简单来说...就是依次排序

17 100 101 85 3

这样几个数

首先,最高位次是3位,所以不够三位的全补0

017 100 101 085 003

准备十个队列,按个位数入队

0 1 2 3 4 5 6 7 8 9

100 101 003 085 017

从左到右依次取出来,新顺序变成

100 101 003 085 017

再根据十位数字入队

0 1 2 3 4 5 6 7 8 9

100 017 085

101

003

从左到右依次取出来,新顺序变成

100 101 003 017 085

再根据百位数字...

0 1 2 3 4 5 6 7 8 9

003 100

017 101

085

取出来...

003 017 085 100 101

deal!

仔细看看,有点类似于 先比个位,再比十位,再比百位

coding!

int getMax(int arr[], int n) {
    int max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] > max) {
            max = arr[i];                    // 寻找最大值
        }
    }
    int digits = 0;
    while (max > 0) {
        digits++;                            //看最大值有几位并返回
        max /= 10;
    }
    return digits;
}

// 基数排序函数
void radixSort(int arr[], int n) {
    // 获取数列中最大值的位数
    int digits = getMax(arr, n);

    // 定义桶数组
    int bucket[10][n];

    // 定义桶中的元素数量
    int bucketCount[10];

    // 初始化桶中元素数量为0
    for (int i = 0; i < 10; i++) {
        bucketCount[i] = 0;
    }

    // 循环遍历每一位数值
    for (int i = 1; i <= digits; i++) {
        // 将每个元素分配到对应的桶中
        for (int j = 0; j < n; j++) {
            int digit = (arr[j] / (int)(pow(10, i - 1))) % 10;
            bucket[digit][bucketCount[digit]] = arr[j];
            bucketCount[digit]++;
        }

        // 将桶中的元素按顺序取出,更新原始数组
        int index = 0;
        for (int j = 0; j < 10; j++) {
            for (int k = 0; k < bucketCount[j]; k++) {
                arr[index] = bucket[j][k];
                index++;
            }
            // 将桶中元素数量重置为0
            bucketCount[j] = 0;
        }
    }
}

排序的稳定性

以下几个数排序

01010202

所有排序方式都能排出来 00001122

问题在于:这个0还是原来那个0吗?这个1还是原来那个1吗?

给之前几个数加个标记

0 1 0 1 0 2 0 2

a b c d

先只看0

如果排序出来的0还是按照abcd的顺序的话,就是稳定的

如果改变了次序的话,就是不稳定的...

定义就是: 如果不因排序而改变相对次序,这个排序就是稳定的

总结

基于比较

时间复杂度

空间复杂度

稳定性

选择

O(n^2)

O(1)

冒泡

O(n^2)

O(1)

插入

O(n^2)

O(1)

归并

O(n logn)

O(n)

快排

O(n logn)

O(log N)

堆排

O(n logn)

O(1)

  • 最低时间复杂度为 O(n logn)

  • 时间复杂度做到O(n logn)且空间复杂度低于O(n)的排序,不可能稳定

要求稳定性用归并排序

要求速度用随机快排

要求空间复杂度低用堆排

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值