八大排序(超详细)

✅博客主页:爆打维c-CSDN博客​​​​​​ 🐾

🔹分享c语言知识及代码

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。常见的排序有直接插入排序,希尔排序,直接选择排序,堆排序,冒泡排序,快速排序,归并排序,计数排序等等,接下来我会为大家逐一介绍比较常见的这八种排序。

下面代码的Swap函数实现如下

void Swap(int* x1, int* x2) {
    int temp = *x1;
    *x1 = *x2;
    *x2 = temp;
}

目录

一.直接插入排序(插入排序)

1.主要排序思想:

2.代码实现

3.直接插入排序的特性总结:

二.希尔排序(插入排序)

1.主要排序思想

2.代码实现

3.希尔排序的特性总结

三. 直接选择排序(选择排序)

1.主要排序思想

2.代码实现

3.选择排序的特性总结

四.堆排序(选择排序)

1.主要排序思想

2.代码实现

3.堆排序的特性总结

五.冒泡排序(交换排序)

1.主要排序思想

2.代码实现

3.冒泡排序的特性总结

六.快速排序

1.主要排序思想

2.代码实现

2.1快速排序递归法

(1) hoare版本

​编辑

(2)挖坑法​编辑

(3)前后指针法(双指针法)

2.2快速排序非递归法

3.快速排序的特性总结:

七.归并排序

1.主要排序思想

2.代码实现

2.1递归实现版本

2.2非递归实现版本

3.归并排序的特性总结:

八.计数排序(非比较排序)

1.主要排序思想

2.代码实现

3.计数排序的特性总结:

总结:排序算法复杂度及稳定性分析


一.直接插入排序(插入排序)

1.主要排序思想:

将数据分为有序、无序两组,将无序组的数据依次向有序组中插入,直到无序组数据全都插入到有序组中,最后得到一个新的有序数列。

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

这种情况类似于我们玩扑克牌,摸到第一张后,将摸到的下一张插入到手上已经有序的牌里

2.代码实现

我们用动图来看看具体怎么实现的

void InsertSort(int* a, int n) {
    int i = 0;
    for (i = 0; i < n - 1; i++) {
        //[0,end]有序 从end+1开始插入前一个有序数组
        int end = i;
        int tmp = a[end + 1];
        while (end >= 0) {
            if (tmp < a[end]) {  
                a[end + 1] = a[end];
                end--;
            }
            else {
                break;
            }
        }
        a[end + 1] = tmp;
    }
}

3.直接插入排序的特性总结:

  • 元素集合越接近有序,直接插入排序算法的时间效率越高
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

二.希尔排序(插入排序)

1.主要排序思想

希尔排序跟插入排序类似,希尔排序法又称缩小增量法。希尔排序法的基本思想是:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

2.代码实现

希尔排序的实现过程如下

代码如下:

void ShellSort(int* a, int n) {
    //实现方式1 从前往后依次来
    //int gap = n;
    //while (gap > 1) {
    //    // +1保证最后一个gap一定是1
    //    // gap > 1时是预排序
    //    // gap == 1时是插入排序
    //    gap = gap / 3 + 1;
    //    int i = 0;
    //    for (i = 0; i < n - gap; i++) {     //从前往后依次来
    //        int end = i;
    //        int tmp = a[end + gap];
    //        while (end >= 0) {
    //            if (tmp < a[end]) {
    //                a[end + gap] = a[end];
    //                end -= gap;
    //            }
    //            else {
    //                break;
    //            }
    //        }
    //        a[end + gap] = tmp;
    //    }
    //}

    //实现方式2 按分组一组一组来
    int gap = n;
    while (gap > 1) {
        // +1保证最后一个gap一定是1
        // gap > 1时是预排序
        // gap == 1时是插入排序
        gap = gap / 3 + 1;
        int i = 0, j = 0;
        for (j = 0; j < gap; j++) {       //一组一组来
            for (i = j; i < n - gap; i += gap) {  
                int end = i;
                int tmp = a[end + gap];
                while (end >= 0) {
                    if (tmp < a[end]) {
                        a[end + gap] = a[end];
                        end -= gap;
                    }
                    else {
                        break;
                    }
                }
                a[end + gap] = tmp;
            }
        }
    }
}

不知道大家发现没有,其实当gap==1时,就是直接插入排序。

3.希尔排序的特性总结

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时是预排序,让数组更接近于有序。当gap == 1时,数组已经接近有序的了,就会增加排序的效率,达到优化的效果。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算。我们取的是gap=gap/3+1,所以时间复杂度大约是O(n^1.3).
  4. 稳定性:不稳定
  5. 空间复杂度:O(1)

三. 直接选择排序(选择排序)

1.主要排序思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

  • 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

2.代码实现

动图演示如下:

void SelectSort(int* a, int n) {
    int left = 0, right = n - 1;
    int mini = 0, maxi = 0;  //默认最小最大下标都为0
    int i = 0;
    while (left <right) {
        mini = left, maxi = left;
        //在左右区间找出最大最小值的下标
        for (i = left; i <=right;i++) {
            if (a[i] < a[mini]) {  //比最小值更小
                mini = i;
            }
            if (a[i] > a[maxi]) {  //比最大值更大
                maxi = i;
            }
        }
	    Swap(&a[left], &a[mini]);
        if (maxi == left) {    //若最大值在最左边 上述已经将最小与最左的数据换位 最大值在最小值下标的地方
            maxi = mini;
        }
        Swap(&a[right], &a[maxi]);
        left++;
        right--;
    }
}

3.选择排序的特性总结

  1. 直接选择排序非常好理解,但效率低下,实际中很少使用。
  2.  时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

四.堆排序(选择排序)

1.主要排序思想

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

2.代码实现

// 堆排序 从小到大 升序建大堆
void AdjustDown(int* a, int n, int root) {  //向下调整算法
    int child = 2 * root + 1;
    while (child < n) {
        if (child + 1 < n && a[child + 1]>a[child]) { //选出大的
            child++;
        }
        if (a[child] > a[root]) {
            //若孩子比父亲大 则交换
            Swap(&a[child], &a[root]);
            //往下继续
            root = child;
            child = 2 * root + 1;
        }
        else {
            break;
        }
    }

}
void HeapSort(int* a, int n) {
    //1.先建堆
    int i = 0;
    for (i = (n - 1 - 1) / 2; i >= 0; i--) {
        AdjustDown(a, n, i);  
    }
    //2.将大堆堆顶最大的数与最后的数换位 再新一轮建堆
    int end = n-1;
    while (end>0) {
        Swap(&a[0], &a[end]);
        AdjustDown(a, end, 0);
        end--;
    }
}

3.堆排序的特性总结

  • 堆排序使用堆来选数,效率较高
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

五.冒泡排序(交换排序)

交换排序基本思想:根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置

交换排序的特点:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。(冒泡排序和快速排序都属于交换排序)

1.主要排序思想

通过对 待排序序列从前向后(从下标较小的元素开始),依次对相邻两个元素的值进行两两比较,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。

2.代码实现

动图演示如下

// 冒泡排序
void BubbleSort(int* a, int n) {
    int i, j,flag=0;
    /*如果中间没有发生交换
    说明前面的值都比后面的小 
    本身就是有序的*/
    for (i = 0; i < n; i++) {
        //共n-1趟
        for (j = 0; j < n - i - 1;j++) {
            //每一趟把最大的放到后面
            if (a[j]> a[j + 1]) {
                Swap(&a[j],&a[j + 1]);
                flag = 1;
            }
        }
        if (flag == 0) {
            break;
        }
    }
}

3.冒泡排序的特性总结

  • 冒泡排序非常容易理解,常用来教学,实际不怎么用。
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

六.快速排序

1.主要排序思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

2.代码实现

2.1快速排序递归法

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
    if(right - left <= 1)
        return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
    int div = PartSort1(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
    QuickSort(array, left, div);
// 递归排[div+1, right)
    QuickSort(array, div+1, right);
}

将区间按照基准值划分为左右两半部分的常见方式有三种,分别是:hoare法,挖坑法,双指针法,下面依次讲解这三种方法

(1) hoare版本

int Mid(int* a, int left, int right) {   
    int mid =left+ (right - left) / 2;  //计算中间索引 避免溢出
    if (a[left] < a[mid]) {
        if (a[mid] < a[right]) {
            return mid;
        }
        else {
            if (a[left] < a[right]) {
                return right;
            }
            else {
                return left;
            }
        }
    }
    else {
        if (a[mid] < a[right]) {
            if (a[right] > a[left]) {
                return left;
            }
            else {
                return right;
            }
        }
        else { return mid; }
    }
}

int PartSort1(int* a, int left, int right) {
    //三数取中 对排序的优化 使keyi趋于中间数值
    int midi = Mid(a, left, right);
    Swap(&a[left], &a[midi]);  //取中之后要交换数值

    int keyi = left;
    int begin = left, end = right;
    while (begin < end) {
        //右边找小
        while (a[end] >= a[keyi] && end > begin) {
            end--;
        }
        //左边找大
        while (a[begin] <= a[keyi] && end > begin) {
            begin++;
        }
        Swap(&a[begin], &a[end]);
    }
    Swap(&a[begin], &a[keyi]);
    return begin;
}

(2)挖坑法

// 快速排序挖坑法
int PartSort2(int* a, int left, int right) {
    //三数取中
    int midi = Mid(a, left, right);
    Swap(&a[left], &a[midi]);  //取中之后要交换数值
    int key =a[left]; 
    int pit = left;   //初始坑位位置

    int begin = left, end = right;
    while (begin < end) {
        //右边找小
        while (a[end] >=key && end > begin) {
            end--;
        }
        Swap(&a[pit], &a[end]); 
        pit = end;
        while (a[begin] <=key && end > begin) {
            begin++;
        }
        Swap(&a[begin], &a[pit]);
        pit = begin;
    }
    a[pit] = key;
    return pit;
}

(3)前后指针法(双指针法)

// 快速排序前后指针法
int PartSort3(int* a, int left, int right) {
    //三数取中
    //int midi = Mid(a, left, right);
    // Swap(&a[left], &a[midi]);  //取中之后要交换数值
    int keyi = left;

    int prev = left;
    int cur = prev + 1;
    while (cur <= right) {
        if (a[cur] < a[keyi] && ++prev != cur) {
            Swap(&a[cur],&a[prev]);
        }
        cur++;
    }
    Swap(&a[prev],&a[keyi]);
    //keyi = prev;
    return prev;
}

上面给出的三数取中是对快速排序算法的优化

快速排序优化:

1. 三数取中法选key
2. 递归到小的子区间时,可以考虑使用插入排序

2.2快速排序非递归法

void QuickSortNonR(int* a, int left, int right)
{
    Stack st;
    StackInit(&st);
    StackPush(&st, left);
    StackPush(&st, right);
    while (StackEmpty(&st) != 0)
    {
        right = StackTop(&st);
        StackPop(&st);
        left = StackTop(&st);
        StackPop(&st);
        if(right - left <= 1)
           continue;
        int div = PartSort1(a, left, right);
        // 以基准值为分割点,形成左右两部分:[left, div) 和 [div+1, right)
        StackPush(&st, div+1);
        StackPush(&st, right);
        StackPush(&st, left);
        StackPush(&st, div);
    }
    StackDestroy(&s);
}

3.快速排序的特性总结:

  • 快速排序整体的综合性能和使用场景都是比较好的,所以叫快速排序
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(logN)     //开辟递归栈帧空间
  • 稳定性:不稳定

七.归并排序

1.主要排序思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

主要步骤如下:

2.代码实现

2.1递归实现版本

void _MergeSort(int* a, int* tmp, int left, int right) {
    if (left >= right) {
        return;
    }
    int mid = left + (right - left) / 2;
    //如果[left,mid][mid+1,right] 均有序则可以进行排序
    _MergeSort(a, tmp, left, mid);
    _MergeSort(a, tmp, mid+1, right);
    //归并
    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;
    int i = left;
    while (begin1 <=end1 && begin2 <=end2) {
        if (a[begin1] <= a[begin2]) {
            tmp[i++] = a[begin1++];
        }
        else {
            tmp[i++] = a[begin2++];
        }
    }
    while (begin1 <= end1) {
        tmp[i++] = a[begin1++];
    }
    while (begin2 <= end2) {
        tmp[i++] = a[begin2++];
    }
    memcpy(a + left, tmp + left, (right-left+1) * sizeof(int));
}

void MergeSort(int* a, int n) {
    //创建一个数组来存放数据
    int* tmp = (int*)malloc(sizeof(int) * n);
    _MergeSort(a, tmp,0,n-1);
    free(tmp);
    tmp = NULL;
}

2.2非递归实现版本

void _MergeSortNonR(int* a,int* tmp,int n) {
    //利用循环来归并 11归并 22归并...
    int gap = 1;
    //gap是间隔几个数据
    while (gap < n) {
        int i = 0;
        for (; i < n; i += 2 * gap) {
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;

            //修正
            if (begin2 >= n) {  //第二组全越界 不需要归并
                break;
            }
            if (end2 >= n) {   //第二组部分越界 调整尾区间
                end2 = n - 1;
            }

            int j = i;   //每一轮的i都在变化 需要重新赋值
            while (begin1 <= end1 && begin2 <= end2) {
                if (a[begin1] <= a[begin2]) {
                    tmp[j++] = a[begin1++];
                }
                else {
                    tmp[j++] = a[begin2++];
                }
            }
            while (begin1 <= end1) {
                tmp[j++] = a[begin1++];
            }
            while (begin2 <= end2) {
                tmp[j++] = a[begin2++];
            }
            memcpy(a + i, tmp +i ,(end2-i+1) * sizeof(int));   //每一组归并完都要及时拷贝 
        }
        gap *= 2;
    }
}
void MergeSortNonR(int* a, int n) {
    //创建一个数组来存放数据
    int* tmp = (int*)malloc(sizeof(int) * n);
    _MergeSortNonR(a,tmp,n);
    free(tmp);
    tmp = NULL;
}

3.归并排序的特性总结:

  • 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  • 时间复杂度:O(N*logN)
  • 空间复杂度:O(N)
  • 稳定性:稳定

八.计数排序(非比较排序)

1.主要排序思想

  • 思想:

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

  • 操作步骤:

1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中

动图演示如下:

2.代码实现

// 计数排序
void CountSort(int* a, int n) {
    int i = 1;
    int min = a[0], max = a[0];
    //遍历找出最大 最小值
    for (; i < n; i++) {
        if (a[i] < min) {
            min = a[i];
        }
        if (a[i] > max) {
            max = a[i];
        }
    }
    int* count = (int*)calloc(max-min+1,sizeof(int));  //max-min+1为数字出现的范围range
    if (count == NULL)
    {
        perror("calloc fail");
        return;
    }

    //统计出现次数
    for (i = 0; i < n; i++) {
        count[a[i] - min]++;
    }

    int j = 0;
    for (i = 0; i <(max-min+1); i++) {

        while (count[i]--) {
            a[j++] = i+min;    //此时 i 的值(表示相对于 min 的偏移量)需加上min
        }
    }
    free(count);
    count = NULL;
}

3.计数排序的特性总结:

  • 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限(只适用于整数)
  • 时间复杂度:O(MAX(N,范围))
  • 空间复杂度:O(范围)
  • 稳定性:稳定

总结:排序算法复杂度及稳定性分析


如果这篇文章对你有帮助的话,请给博主一个免费的赞鼓励一下吧~ 本文仅简单介绍了有关排序的一些基本概念和相关代码实现,以上个人拙见,若有错误之处,希望各位能提出宝贵的建议和更正,感谢您的观看!

评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值