C语言数据结构与算法笔记(排序算法)

news2024/9/22 15:30:00

排序算法

基础排序

冒泡排序

核心为交换,通过不断进行交换,将大的元素一点一点往后移,每一轮最大的元素排到对应的位置上,形成有序。
设数组长度为N,过程为:

  • 共进行N轮排序
  • 每一轮排序从数组的最左边开始,两两元素进行比较,左边元素大于右边元素,就交换两个元素的位置,否则不变。
  • 每轮排序都会将剩余元素中最大的一个推到最右边,下次排序就不再考虑对应位置的元素。

注意交换不能直接进行,需要中间元素。
实际上排序不需要N轮,N-1轮即可,最后一轮只有一个元素未排序。

// 冒泡排序
void BubbleSort(int arr[], int size)
{
    for(int i = 0; i < size-1 ;++i) // 减1是不考虑最后一次交换
    {
        for(int j = 0 ; j < size - i - 1; ++j )
        {
            if(arr[j] > arr[j+1])
            {
                int tmp = arr[j]; // 加入中间元素tmp进行交换
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
            }
        }
    }
}

int main()
{
    int size = 7;
    int arr1[] = {2,4,1,7,4,9,3};
    BubbleSort(arr1, size);
    for(int i = 0 ; i < size;++i)
    {
        printf("%d ", arr1[i]);
    }
}

优化:如果整轮排序中没有出现任何交换,则说明数组是有序的,内层循环中加入标记
没有发生任何交换,则flag一定是1,数组有序

// 改进:没有出现交换则已经有序
void BubbleSort1(int arr[], int size)
{
    for(int i = 0 ; i < size - 1; ++i)
    {
        _Bool flag = 1; // 加入标记
        for(int j = 0 ; j < size - 1 - i; ++j)
        {   
            int tmp =arr[j];
            arr[j] = arr[j+1];
            arr[j+1] = tmp;
        }
        if(flag)
        {
            break;
        }
    }
}

排序稳定性,大小相同的两个元素在排序前和排序后的先后顺序不变,则排序算法就是稳定的。比如以上的冒泡排序法只会在前者大于后者的情况下才会发生交换,不会影响到相等的两个元素。

插入排序

类似于斗地主的插牌。默认一开始只有第一张牌是有序的,剩余部分进行遍历,然后插到前面对应的位置上。
设数组长度为N

  • 一共进行N轮排序
  • 每轮排序会从后面依次选择一个元素,与前面已经处于有序的元素,从后往前比较,直到遇到一个不大于当前元素的元素,将当前元素插入到此元素的前面。
  • 插入元素后,后续元素则全部后移一位。
  • 当后面所有元素全部遍历完成,全部插入到对应位置之后结束排序。
// 插入排序
void InsertSort(int arr[], int size)
{
    for(int i = 1; i < size -1; ++i) // 从第2个元素开始
    {
        int tmp = arr[i], j = i;
        while (j > 0 && arr[j-1] > tmp) // 只要j>0并且前一个元素大于当前元素
        {
            arr[j] = arr[j-1]; // 交换前一个元素
            j--;
        }
        arr[j] = tmp;
    }
}
int main()
{
    int arr1[] = {2,1,8,5,6,4};
    InsertSort(arr1, 6);
    printArray(arr1, 6);     
}

改进:寻找插入位置上逐个比较,花费时间长,如果前面一部分元素已经是有序状态,可以考虑使用二分搜索算法来查找对应的插入位置,节省插入点的时间。

// 二分搜索法
int BinarySearch(int arr[], int left, int right, int target)
{
    int mid;
    while(left <= right)
    {
        mid = (left + right) / 2;
        if(target == arr[mid])
        {
            return mid + 1;
        }
        else if (target < arr[mid])
        {
            right = mid - 1; // 目标值小于中间的值,往左边去找
        }
        else
        {
            left = mid + 1; // 往右边去找
        }
    }
    return left; // 二分划分范围,left就是插入的位置
}
// 改进的插入排序
void InsertSort1(int arr[], int size)
{
    for(int i = 0 ; i < size; ++i)
    {
        int tmp = arr[i];
        int j = BinarySearch(arr,0,size-1,arr[i]); // 二分搜索查找插入的位置
        for( int k = i ; k > j; k--)
        {
            arr[k] = arr[k-1]; // 往后移
        }
        arr[j] = tmp;
    }
}

算法稳定性,在优化前的插入排序,实际上是不断向前寻找一个不大于待插入元素的元素,相等时只会插入到其后面,不会修改相等元素的顺序;而改进后的二分搜索法,可能会将两个连续相等元素分割开来。

选择排序

每次都去后面找一个最小的放到前面。
设数组长度为N

  • 共进行N轮排序
  • 每轮排序会从后面的所有元素中寻找一个最小的元素,与已经排序好的下一个位置进行互换
  • 进行N轮交换后,得到有序数组
// 选择排序
void SelectSort(int arr[], int size)
{
    for(int i = 0 ; i < size - 1; ++i) // N-1轮排序
    {
        int min = i ; // 记录当前最小的元素,默认是剩余元素中的第一个
        for(int j = i + 1; j < size;++j)
        {
            if(arr[min] > arr[j])
            {
                min = j; 
            }
            int tmp = arr[i]; // 找出最小元素之后,开始交换
            arr[i] = arr[min];
            arr[min] = tmp;
        }
    }
}
// 打印
void printArray(int arr[],int size)
{
    for(int i = 0 ; i < size;++i)
    {
        printf("%d ", arr[i]);
    }
}
int main()
{
    int arr1[] = {2,9,6,8,3,6,5};
    SelectSort(arr1 , 7);
    printArray(arr1, 7);
}

改进:因为每次需要选一个最小的,不妨顺便选个最大的,小的往左边丢,大的往右边丢。

// 交换
void swap(int* a, int*b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
// 优化的选择排序
void SelectSort1(int arr[], int size)
{
    int left=0, right = size - 1; // 假设左右排好序,往中间缩小
    while (left < right)
    {
        int max = right, min = left;
        for(int i = left; i < right; ++i)
        {
            // 同时找最大和最小的
            if(arr[i] < arr[min])
            {
                min = i;
            }
            if(arr[i] > arr[max])
            {
                max = i;
            }
        }
        swap(&arr[max], &arr[right]); // 先把大的换到右边
        // 大的换到右边之后,有可能被换出来的是最小的,需要判断以下
        // 如果遍历完最小的是当前右边排序的第一个元素
        // 将min换到那个位置
        if(min == right)
        {
            min = max;
        }
        swap(&arr[min], &arr[left]);
        left++;
        right--;
    }  
}

稳定性:由于每次寻找的是最小的元素,向前插入时会发生交换操作,当存在两个连续相等元素,破坏了原有的顺序。不稳定的。

比较三种基础排序

冒泡排序(优化后)

  • 最好情况时间复杂度:O(n),本身是有序的,只需要一次遍历。
  • 最坏情况时间复杂度:O(n^2),倒序。
  • 空间复杂度:O(1),只需要一个变量存储需要交换的变量
  • 稳定
    插入排序
  • 最好情况时间复杂度:O(n),本身是有序的,插入的位置也是同样的位置,不变动任何元素
  • 最坏情况时间复杂度:O(n^2),倒序。
  • 空间复杂度:O(1),只需要一个变量存储抽出来的元素
  • 稳定
    选择排序
  • 最好情况时间复杂度:O(n^2),即使数组本身是有序的,每一轮还得将剩余部分依次找完才确定最小的元素
  • 最坏情况时间复杂度:O(n^2)
  • 空间复杂度:每一轮需要记录最小元素位置,空间复杂度为O(1)
  • 不稳定

进阶排序

快速排序

快速排序是冒泡排序的进阶版,由于冒泡排序是对相邻元素进行比较和交换,每次只能移动一个位置,效率相对较低;而快速排序是从两端向中间进行,一轮就可将较小的元素交换到左边,较大的元素交换到右边。

实际上每一轮目的就是将较大的丢到基准右边,较小的丢到基准左边

  • 一开始排序为整个数组
  • 排序之前,以第一个元素作为基准
  • 从最右边向左看,依次将每一个元素与基准元素进行比较,如果该元素比基准元素小,就与左边遍历位置上的元素(一开始为基准元素位置)进行交换,保留右边当前遍历的位置
  • 交换后,转为从左边往右开始遍历元素,如果发现比基准元素大,则与之前保留右边遍历的位置上元素进行交换,同样保留左边当前遍历的位置
  • 当左右遍历撞到一起,本轮快速排序完成,中间的位置元素就是基准元素
  • 以基准位置为中心,划分左右两边,同样方式进行

代码实现

// 快速排序
void QuickSort(int arr[], int start, int end)
{
    if(start >= end) // 不满足初始位置则返回
    {
        return;
    }
    int left = start, right = end; // 定义两个指向左右两个端点的指针
    int pivot = arr[left]; // 预先确定基准点为左端第一个元素
    while (left < right)
    {
        while(left < right && arr[right] >= pivot)
        {
            right--;// 从右往左看
        }
        arr[left] = arr[right]; // 比基准值小就放到左边去
        while (left < right&& arr[left]  <= pivot)
        {
            left++; // 从左往右看
        }
        arr[right] = arr[left]; // 比基准值大就放到右边
        arr[left] =pivot; //相遇位置即为基准存放的位置
    }
    QuickSort(arr, start , left-1); //划分基准左边
    QuickSort(arr, left+1, end); // 划分基准右边, 再次进行快速排序
}

测试

int main()
{
    int arr1[]= {9,3,6,3,4,8,1,2};
    QuickSort(arr1, 0 , 8);
    for(int i = 0; i < 8 ; ++i)
    {
        printf("%d ", arr1[i]);
    }
}

双轴快速排序

快速排序的升级版,双轴快速排序,可对大数组进行。如果遇到数组完全倒序的情况
在这里插入图片描述
每一轮需要完整遍历整个范围,每一轮最大或最小的元素被推向两边,则此完全倒序情况快速排序退化为冒泡排序。为解决这种极端情况,再添加一个基准元素,使得数组可分为三段。
在这里插入图片描述
分为三段后,每轮双轴排序结束后对三段继续进行双轴快速排序。该适用于那些量比较大的数组。

首先取出首元素和尾元素作为两个基准,对其进行比较,若基准1大于基准2,先交换两个基准。
在这里插入图片描述
需要创建三个指针
在这里插入图片描述
从橙色指针所指元素开始进行判断,

  • 小于基准1,那需要先将蓝色指针向后移,把元素交换到蓝色指针那去,然后橙色指针也向后移动
  • 不小于基准1且不大于基准2,直接把橙色指针向前移动即可
  • 大于基准2,需要丢到右边去,先将右边指针左移,不断向前找到一个比基准2小的,进行交换
    橙色指针与绿色指针之间即为待排序区域
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    代码实现:
void swap(int* a, int*b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void dualPivotQuickSort(int arr[], int start, int end)
{
    if(start >= end)
    {
        return; // 结束条件
    }
    if(arr[start] > arr[end]) // 首尾两个基准比较
    {
        swap(&arr[start], &arr[end]); // 大的换到后面
    }
    int pivot1 = arr[start], pivot2 = arr[end]; // 取出两个基准元素
    int left = start, right = end, mid = left + 1; // 分三个区域,三个指针
    while (mid < right)
    {
        if(arr[mid] < pivot1) // mid所指向元素小于基准1,需要放到最左边
        {
            swap(&arr[++left], &arr[mid++]); // 和最左边交换,left 和 mid向前移动
        }
        else if(arr[mid] <= pivot2) // 不小于基准1但小于基准2,在中间
        {
            mid++; // 本身在中间,向前移动以缩小范围
        }
        else // 右边的情况
        {
            while (arr[--right] > pivot2 && right > mid); // 先移动右边指针,需要右边位置来存放需要换过来的元素
            
            if(mid >= right)
            {
                break; // 剩余元素找完,没有比基准2小的,可直接结束
            }
            swap(&arr[mid], &arr[right]); // 还有剩余元素,找到比基准2小的,直接交换   
        }
    } 
    swap(&arr[left], &arr[start]); // 基准1与left交换,基准1左边元素都比其小
    swap(&arr[right],&arr[end]); // 基准2与right交换,基准2右边元素都比其大
    // 继续对剩下三个区域双轴快速排序
    dualPivotQuickSort(arr, start,left-1);
    dualPivotQuickSort(arr, left+1, right-1);
    dualPivotQuickSort(arr, right+1, end);
}
    dualPivotQuickSort(arr1, 0, 8);
    for(int i = 0; i < 8 ; ++i)
    {
        printf("%d ", arr1[i]);
    }

希尔排序(缩小增量排序)

直接插入排序的进阶版,极端情况会出现让所有已排序元素后移的情况(比如刚好要插入的是一个特别小的元素),为解决这种问题,对整个数组按照步长进行分组,优先比较距离较远的元素。

步长是由一个增量序列,当增量序列一般使用 n 2 、 n 4 、 n 8 . . . 、 1 \frac{n}{2}、\frac{n}{4}、\frac{n}{8}...、1 2n4n8n...1这样的序列。
设数组长度为N,详细过程为:

  1. 求出最初步长,n/2
  2. 整个数组按照步长进行分组,两两一组(n为奇数,第一组有三个元素)
  3. 分别在分组内插入排序
  4. 排序后,将步长/2,重新分组,重复上述步骤,直到步长为1,插入排序最后一遍结束
    在这里插入图片描述
    插入排序后,小的元素尽可能地向前走,缩小步长,4/2=2
    在这里插入图片描述
    代码实现
// 希尔排序
void shellSort(int arr[], int size)
{
    int delta = size / 2;
    while (delta >= 1) // 使用之前的插入排序,此时需要考虑分组
    {
        for(int i = delta; i < size; ++i) // 从delta开始,前delta个组的第一个元素默认是有序状态
        {
            int j = i, tmp = arr[i]; // 依然是把待插入的先抽出来
            while (j >= delta && arr[j - delta] > tmp) 
            {
                // 需要按步长往回走,所以是j-delta,j必须大于等于delta才可以,j-delta小于0说明前面没有元素
                arr[j] = arr[j - delta];
                j -= delta;
            }
            arr[j] = tmp;
        }
        delta /= 2; // 分组插排结束之后,再计算步长
    }
} 
int main()
{
    int arr[] = {3,5,7,2,9,0,6,1,8,4};
    shellSort(arr, 10);
    for(int i = 0 ; i < 10; ++i)
    {
        printf("%d ", arr[i]);
    }
}

尽管有循环多次,但时间复杂度比O(n^2)小,小的元素往左靠。希尔排序不稳定,因为按步长分组,有可能相邻得两个相同元素,后者在自己组内被换到前面去。

堆排序

选择排序一种,但能比选择排序更快。
小根堆(小顶堆),对一棵不完全二叉树,树中父亲结点都比孩子结点小;大根堆(大顶堆)树中父亲结点都比孩子节点大。
堆是一棵完全二叉树,数组来表示
在这里插入图片描述
构建一个堆,将一个无序的数组依次输入,最后存放的序列是一个按顺序排放的序列。

但仍需要额外O(n)的空间作为堆,可以对其进一步优化,减少空间上的占用。直接对给定的数组进行堆的构建
设数组长度为N

  • 将给定数组调整为大顶堆
  • 进行N轮选择,每次选择大顶堆顶端元素从数组末尾开始向前存放(交换堆顶和堆的最后一个元素)
  • 交换完成后,重新对堆的根节点进行调整,使其继续满足大顶堆的性质
  • 当N轮结束后,得到从小到大的数组

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1527333.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云原生(三)、Docker网络

Docker网络 在 Docker 中&#xff0c;不同容器之间的网络访问原理取决于容器所使用的网络模式。下面是 Docker 中常见的两种网络模式下容器间网络访问的原理&#xff1a; 桥接模式&#xff08;Bridge&#xff09;&#xff1a; 在桥接模式下&#xff0c;Docker 使用 Linux 桥接…

Vue3学习日记 Day4 —— pnpm,Eslint

注&#xff1a;此课程需要有Git的基础才能学习 一、pnpm包管理工具 1、使用原因 1.1、速度快&#xff0c;远胜过yarn和npm 1.2、节省磁盘空间 2、使用方式 2.1、安装方式 npm install -g pnpm 2.2、创建项目 pnpm create vue 二、Eslint配置代码风格 1、环境同步 1、禁用Pret…

Jenkins实现CICD(3)_Jenkins连接到git

文章目录 1、如何完成上述操作&#xff0c;并且不报如下错&#xff1a;2、连接不上git&#xff0c;操作如下&#xff1a;3、将上边产生的3个文件拷贝到&#xff1a;C:\Windows\System32\config\systemprofile\.ssh4、新建下图凭证&#xff1a;创建步骤&#xff1a; 5、公钥填到…

OpenCV 新版滴 4.5.1 发布啦!

发布亮点&#xff1a; OpenCV Github 项目终于突破50000 stars&#xff01;新的里程碑~ 这次发布的特性包括&#xff1a; 集成更多的GSoC 2020 项目的结果&#xff0c;包括&#xff1a; 开发了OpenCV.js DNN 模块&#xff0c;以方便再网页中使用&#xff0c;并提供了相关教…

基于yolov2深度学习网络的人脸检测matlab仿真,图像来自UMass数据集

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 网络架构与特征提取 4.2 输出表示 4.3损失函数设计 4.4预测阶段 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 load yolov2.mat% 加载…

浏览器架构的温故知新

【引子】前端可能是一个日新月异的领域&#xff0c;我们很难了解其中的方方面面。但是&#xff0c;前端系统一般都以浏览器作为运行环境&#xff0c; 对浏览器的进一步理解有助于我们更好地开发前端应用。这也是本文的由来之一&#xff0c;也作为对runtime的一次实例分析。 浏览…

全国各省市县统计年鉴/中国环境统计年鉴/中国工业企业数据库/中国专利数据库/污染排放数据库

统计年鉴是指以统计图表和分析说明为主&#xff0c;通过高度密集的统计数据来全面、系统、连续地记录年度经济、社会等各方面发展情况的大型工具书来获取统计数据资料。 统计年鉴是进行各项经济、社会研究的必要前提。而借助于统计年鉴&#xff0c;则是研究者常用的途径。目前国…

Web3 之力:探索去中心化技术的创新应用

在当今数字化时代&#xff0c;随着区块链技术的发展和应用&#xff0c;Web3作为其重要组成部分&#xff0c;正在逐渐改变着我们对于互联网和数字经济的认知与体验。Web3不仅是一种技术革新&#xff0c;更是一种新的思维范式&#xff0c;其去中心化的特点为数字世界带来了更多的…

DOcker搭建Rancher

简介 Rancher 是供采用容器的团队使用的完整软件堆栈。它解决了管理多个Kubernetes集群的运营和安全挑战&#xff0c;并为DevOps团队提供用于运行容器化工作负载的集成工具。 官网地址&#xff1a;https://www.rancher.cn/ 安装 拉取镜像 docker pull rancher/rancher:stab…

Chrome历史版本下载地址:Google Chrome Older Versions Download (Windows, Linux Mac)

最近升级到最新版本Chrome后发现页面居然显示错乱,是在无语, 打算退回原来的版本, 又发现官方只提供最新的版本下载, 为了解决这个问题所有收集了Chrome历史版本的下载地址分享给大家. Google Chrome Windows version 32-bit VersionSizeDate104.0.5112.10279.68 MB2022-05-30…

汽车电子拓扑架构的演进过程

汽车电子拓扑架构的演进过程 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师 (Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,喝完再挣扎,出门靠…

【Sass】1px分割线 + 缩进分割线

效果图 1. 亮色模式效果 2. 暗色模式效果 设计思路 配色使用grey色 优点&#xff1a;无论在暗色模式还是亮色模式都可以看清楚分割线 使用after,before 伪元素绘制线条&#xff0c;并压缩线条transform: scaleY(.25) 注意事项 必须确保父级有宽高父级定位必须为position: r…

【地图】腾讯地图 - InfoWindow 自定义信息窗口内容时,内容 html 嵌套混乱问题

目录 需求描述问题问题代码页面展示 解决原因解决办法解决代码页面展示 代码汇总注 需求描述 腾讯地图上画点位&#xff0c;点击点位展示弹框信息 问题 问题代码 // 打开弹框 openInfoWindow(position, content) {this.infoWindow new TMap.InfoWindow({map: this.map,posit…

使用jenkins-pipeline进行利用项目文件自动化部署到k8s上

Discard old builds:丢弃旧的构建,目的是管理存储空间、提升性能以及保持环境整洁 Do not allow concurrent builds: 禁止并发构建是指同一时间内只允许一个构建任务执行,避免多个构建同时运行可能带来的问题 Do not allow the pipeline to resume if the controller resta…

macOS 通过 MacPorts 正确安装 MySQL 同时解决无法连接问题

如果你通过 sudo port install 命令正常安装了 MySQL&#xff0c;再通过 sudo load 命令启动了 MySQL Server&#xff0c;此刻却发现使用 Navicat 之类的 GUI 软件无法连接&#xff0c;始终返回无法连接到 127.0.0.1 服务器。这是一个小坑&#xff0c;因为他默认使用了 Sock 套…

初级爬虫实战——哥伦比亚大学新闻

文章目录 发现宝藏一、 目标二、简单分析网页1. 寻找所有新闻2. 分析模块、版面和文章 三、爬取新闻1. 爬取模块2. 爬取版面3. 爬取文章 四、完整代码五、效果展示 发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不…

docker小白第十三天-compose容器编排

docker-compose容器编排 Docker-Compose是Docker官方的开源项目&#xff0c;负责实现对Docker容器集群的快速编排。Compose是Docker公司推出的一个工具软件&#xff0c;可以管理多个Docker容器组成一个应用。你需要定义一个YAML格式的配置文件docker-compose.yml&#xff0c;写…

信驰达车规蓝牙模块RF-BM-2642QB1I赋能汽车T-Box

近年来&#xff0c;随着人们对数据传输需求的增长&#xff0c;传统网络布线的通讯方式逐渐显现出满足不了的局限性&#xff0c;与此同时&#xff0c;各种无线传输技术迅速发展。汽车工业同样需要无线通讯技术&#xff0c;但红外技术、802.11、HomeRF等技术在汽车工业中存在一定…

ASP.NET通过Appliaction和Session统计在人数和历史访问量

目录 背景: Appliaction&#xff1a; Session&#xff1a; 过程&#xff1a; 数据库&#xff1a; Application_Start&#xff1a; Session_Start&#xff1a; Session_End&#xff1a; Application_End&#xff1a; 背景: 事件何时激发Application_Start在调用当前应用…

学习笔记Day8:GEO数据挖掘-基因表达芯片

GEO数据挖掘 数据库&#xff1a;GEO、NHANCE、TCGA、ICGC、CCLE、SEER等 数据类型&#xff1a;基因表达芯片、转录组、单细胞、突变、甲基化、拷贝数变异等等 常见图表 表达矩阵 一行为一个基因&#xff0c;一列为一个样本&#xff0c;内容是基因表达量。 热图 输入数据…