当前位置: 首页 > news >正文

C语言数据结构易错知识点(6)(快速排序、归并排序、计数排序)

快速排序属于交换排序,交换排序还有冒泡排序,这个太简单了,这里就不再讲解。

归并排序和快速排序都是采用分治法实现的排序,理解它们对分支思想的感悟会更深

计数排序属于非比较排序,在数据集中的情况下可以考虑使用。这时效率也比较高。

 

下面讲解一下这三种排序在代码实现上易错的地方:

一、快速排序

快速排序通过每趟排序确定一个数的最终位置最终实现将数组变得有序的功能。主要有三种方法,第一种方法偏向常规,第二种方法是双指针法,第三种方法是非递归法,除此之外,还有一些优化的方案,如挖坑法,随机确定key,三数取中,后面代码实现上会简要说明。

方法一:常规法

1.代码实现:


void QuickSort1(int* arr, int left, int right)
{int start = left, end = right, key = left;if (left >= right)return;while (left < right){while (left < right && arr[right] >= arr[key])right--;while (left < right && arr[left] <= arr[key])left++;swap(&arr[left], &arr[right]);}swap(&arr[key], &arr[left]);key = left;QuickSort1(arr, start, key - 1);QuickSort1(arr, key + 1, end);
}

2.下面是单趟排序的逻辑:

2aa774f668f8497b97894ffcadca0e70.png

至于需要注意的点,就是当key在左侧时,一定要右侧先动,左侧再动,这样才能保证相遇点的值小于等于arr[key](这个结论可以画图证明,这里就不展开了),当然,逆序同理。

3.下面是整体逻辑的详细解读,写代码时一定要注意细节,后续快排代码不再重复:

4978faa9cd9e4b35b23afafac2f5ef67.png

4.由于相遇点的值小于等于arr[key]这个结论一定程度上难以理解,于是就出现了一种优化方案——挖坑法,代码如下:


void QuickSort5(int* arr, int left, int right)
{if (left >= right)return;int start = left, end = right, key = left, piti = left;//存储坑的下标int tmp = arr[key];//用来初始坑里的值while (left < right){while (left < right && arr[right] >= tmp)right--;if (left < right){arr[piti] = arr[right];piti = right;}while (left < right && arr[left] <= tmp)left++;if (left < right){arr[piti] = arr[left];piti = left;}}arr[piti] = tmp;key = piti;QuickSort5(arr, start, key - 1);QuickSort5(arr, key + 1, end);
}

5.当数组接近有序时,每次调用后key的位置都会很偏,导致递归次数接近N,总时间复杂度来到O(N ^ 2),因此取key尤为关键,关键在于不要一来就取到最小值,因此随机取key和三数取中可以一定程度上缓解这个问题:,代码如下:

三数取中:


int GetMid(int* arr, int left, int right)
{int mid = (left + right) / 2;if (arr[mid] >= arr[left]){if (arr[mid] < arr[right])return mid;else{if (arr[left] < arr[right])return right;elsereturn left;}}else{if (arr[mid] > arr[right])return mid;else{if (arr[right] < arr[left])return left;elsereturn right;}}}

随机取key:

int key = rand() % (right - left + 1) + left;//随机值

但是这些方法依旧不能完全消除快排的缺陷,当数组是1111111111这种时,时间复杂度依然会变成O(N ^ 2)

6.时间复杂度分析

快排的时间复杂度是O(NlogN),分析方法如下:

1abdf1502e1641ca8d215a2406310bbf.png

需要补充的是,logN与N的差别很大,N-logN依然是N的量级,举个例子:已知2的10次方是1024,假如有1024个数据,在快排中,单趟排序的遍历次数为1024、1023、1022一直到1014(末项可能会更小,但不会小多少),我们发现,这些数字差别很小,几乎是由N来决定的,所以上面图中说我们一般可以将单趟遍历的次数看作N

方法二:双指针法

这个方法的代码实现比较简单,关键在于prev指针和cur指针之间嵌入大于arr[key]的值,注意每次arr[prev]的下一个元素都是大于arr[key]的(当然也有可能相同或者没有元素,但是在这两种特殊情况下对我们功能的实现没有影响),最后交换arr[key]和arr[prev]达到一样的效果,使右侧大于arr[key],左侧小于等于arr[key]。后续逻辑相同。

代码实现如下:


void QuickSort3(int* arr, int left, int right)
{int prev = left, cur = left + 1, key = left;if (left > right)return;while (cur <= right){if (arr[cur] > arr[key])cur++;elseswap(&arr[++prev], &arr[cur++]);}swap(&arr[key], &arr[prev]);key = prev;QuickSort3(arr, left, key - 1);QuickSort3(arr, key + 1, right);
}

方法三:非递归法

这种方法利用了前序遍历和栈的相似性。为什么说是前序遍历呢?因为在遍历时,该层遍历还同时保存了左递归和右递归的必要信息,就像二叉树的父节点能够找到子节点,而子节点找不到父节点。如果是中序或后序就没有这样的特性,就不适合用栈或队列等数据结构来实现。这也是为什么归并排序不适合用栈和队列来实现非递归。

代码如下:


void QuickSort4(int* arr, int left, int right)
{Stack s;StackInit(&s);StackPush(&s, right), StackPush(&s, left);//从右向左入栈while (!isStackEmpty(&s)){int left = StackTop(&s);StackPop(&s);int right = StackTop(&s);StackPop(&s);int prev = left, cur = left + 1, key = left;//从左向右取数据while (cur <= right){if (arr[cur] > arr[key])cur++;elseswap(&arr[++prev], &arr[cur++]);}swap(&arr[key], &arr[prev]);key = prev;if(key + 1 < right)StackPush(&s, right), StackPush(&s, key + 1);//后取得if(left < key - 1)StackPush(&s, key - 1), StackPush(&s, left);//先取得}StackDestroy(&s);
}

相关栈的功能实现如下:


void StackInit(Stack* ps)
{ps->arr = NULL;ps->capacity = ps->size = 0;
}void StackPush(Stack* ps, int val)
{if (ps->capacity == ps->size){ps->capacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;int* tmp = (int*)realloc(ps->arr, sizeof(int) * ps->capacity);assert(tmp);ps->arr = tmp;}ps->arr[ps->size++] = val;
}int StackTop(Stack* ps)
{return ps->arr[ps->size - 1];
}void StackPop(Stack* ps)
{ps->size--;
}bool isStackEmpty(Stack* ps)
{return ps->size == 0;
}void StackDestroy(Stack* ps)
{free(ps->arr), ps->arr = NULL;ps->capacity = ps->size = 0;}

需要注意入栈顺序是从右向左,右下标先入,左下标后入,右递归先入,左递归后入。基本逻辑和递归相似,理解了原理就很好写。

二、归并排序

归并排序一定程度上比较好理解,但非递归法需要注意的细节比较多

方法一:递归法


void _MergeSort(int* arr, int* tmp, int start, int end)
{if (start == end)return;int mid = (start + end) / 2;_MergeSort(arr, tmp, start, mid);_MergeSort(arr, tmp, mid + 1, end);int start1 = start, end1 = mid;int start2 = mid + 1, end2 = end;int i = start1;while (start1 <= end1 && start2 <= end2){if (arr[start1] <= arr[start2])tmp[i++] = arr[start1++];elsetmp[i++] = arr[start2++];}while (start1 <= end1){tmp[i++] = arr[start1++];}while (start2 <= end2){tmp[i++] = arr[start2++];}memmove(arr + start, tmp + start, sizeof(int) * (end - start + 1));
}void MergeSort1(int* arr, int size)
{int* tmp = (int*)malloc(sizeof(int) * size);assert(tmp);_MergeSort(arr, tmp, 0, size - 1);free(tmp), tmp = NULL;
}

整体逻辑:

0de86724086243989b9150329223f24f.png

7e75af49074e4b4b87edbf835f8b5a28.png

方法二:非递归法

非递归法理解起来较为困难,下面详细分析:

代码实现:


void MergeSort2(int* arr, int size)
{int* tmp = (int*)malloc(sizeof(int) * size);assert(tmp);int gap = 1;while (gap <= size){for (int i = 0; i < size; i += 2 * gap){int start1 = i, end1 = start1 + gap - 1;int start2 = end1 + 1, end2 = start2 + gap - 1;int j = start1;if (end1 >= size || start2 >= size)break;while (end2 >= size)end2--;while (start1 <= end1 && start2 <= end2){if (arr[start1] <= arr[start2])tmp[j++] = arr[start1++];elsetmp[j++] = arr[start2++];}while (start1 <= end1){tmp[j++] = arr[start1++];}while (start2 <= end2){tmp[j++] = arr[start2++];}memmove(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(tmp), tmp = NULL;
}

1.整体逻辑

ace813579db94594a8961f1fb9b515d1.png

它的逻辑大体上和递归法相似,但非递归法是直接从最小元素开始向上归并。

2.非递归归并的难点在于数组前面归并是两两为一组,会导致有落单的情况,需要进行处理,上面这张图就展示出了一种情况,下面分析一下:

90cef48aca034af8b525250f38bf479d.png

三、计数排序

这个排序非常简单,不做过多分析

代码实现:


void CountSort(int* arr, int size)
{int max = arr[0], min = arr[0];for (int i = 0; i < size; i++){if (arr[i] > max)max = arr[i];if (arr[i] < min)min = arr[i];}int range = max - min + 1;int* count = (int*)calloc(range, sizeof(int));assert(count);for (int i = 0; i < size; i++){count[arr[i] - min]++;}for (int i = 0, j = 0; i < range; i++){while (count[i]--){arr[j++] = i + min;}}}

注意这个方法用于数据集中时使用,这个时候效率很高。但分散数据就别用,会浪费大量空间。这个排序讨论其稳定性没有意义,因为它只能做到数值的排序,这些排序后没有意义。

 

相关文章:

C语言数据结构易错知识点(6)(快速排序、归并排序、计数排序)

快速排序属于交换排序&#xff0c;交换排序还有冒泡排序&#xff0c;这个太简单了&#xff0c;这里就不再讲解。 归并排序和快速排序都是采用分治法实现的排序&#xff0c;理解它们对分支思想的感悟会更深。 计数排序属于非比较排序&#xff0c;在数据集中的情况下可以考虑使…...

使用 React Router v6.22 进行导航

使用 React Router v6.22 进行导航 React Router v6.22 是 React 应用程序中最常用的路由库之一&#xff0c;提供了强大的导航功能。本文将介绍如何在 React 应用程序中使用 React Router v6.22 进行导航。 安装 React Router 首先&#xff0c;我们需要安装 React Router v6…...

单链表的插入和删除

一、插入操作 按位序插入&#xff08;带头结点&#xff09;&#xff1a; ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。 typedef struct LNode{ElemType data;struct LNode *next; }LNode,*LinkList;//在第i 个位置插插入元素e (带头结点) bool Li…...

全量知识系统 之“程序”详细设计 之 “絮”---开端“元素周期表”表示的一个“打地鼠”游戏

全量知识系统 之“程序”详细设计 概述-概要和纪要 序 絮&#xff08;一个极简的开场白--“全量知识系统”自我介绍&#xff09; 将整个“人生”的三个阶段 比作“幼稚园”三班 &#xff1a; 第一步【想】-- “感性”思维游戏&#xff1a;打地鼠 。学前教育-新生期&#x…...

【详细讲解WebView的使用与后退键处理】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…...

【Linux多线程】生产者消费者模型

【Linux多线程】生产者消费者模型 目录 【Linux多线程】生产者消费者模型生产者消费者模型为何要使用生产者消费者模型生产者消费者的三种关系生产者消费者模型优点基于BlockingQueue的生产者消费者模型C queue模拟阻塞队列的生产消费模型 伪唤醒情况&#xff08;多生产多消费的…...

Django屏蔽Server响应头信息

一、背景 最近我们被安全部门的漏洞扫描工具扫出了一个服务端口的漏洞。这个服务本身是一个Django启动的web服务&#xff0c;并且除了登录页面&#xff0c;其它页面或者接口都需要进行登录授权才能进行访问。 漏洞扫描信息和提示修复信息如下: 自然这些漏洞如何修复&#xff0c…...

前端对数据进行分组和计数处理

js对数组数据的处理&#xff0c;添加属性&#xff0c;合并表格数据。 let data[{id:1,group_id:111},{id:2,group_id:111},{id:3,group_id:111},{id:4,group_id:222},{id:5,group_id:222} ]let tempDatadata; tempDatatempData.reduce((arr,item)>{let findarr.find(i>i…...

synchronized 和 lock

synchronized 和 Lock 都是 Java 中用于实现线程同步的机制&#xff0c;它们都可以保证线程安全。 # synchronized 介绍与使用 synchronized 可用来修饰普通方法、静态方法和代码块&#xff0c;当一个线程访问一个被 synchronized 修饰的方法或者代码块时&#xff0c;会自动获…...

ssh 公私钥(github)

一、生成ssh公私钥 生成自定义名称的SSH公钥和私钥对&#xff0c;需要使用ssh-keygen命令&#xff0c;这是大多数Linux和Unix系统自带的标准工具。下面&#xff0c;简单展示如何使用ssh-keygen命令来生成具有自定义名称的SSH密钥对。 步骤 1: 打开终端 首先&#xff0c;打开我…...

LangChain入门:8.打造自动生成广告文案的应用程序

在这篇技术博文中,我们将探讨如何利用LangChain框架的模板管理、变量提取和检查、模型切换以及输出解析等优势,打造一个自动生成广告文案的应用程序。 LangChain框架的优势 在介绍应用程序之前,让我们先了解一下LangChain框架的几个优势: 模板管理: 在大型项目中,文案可…...

AI如何影响装饰器模式与组合模式的选择与应用

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》《MYSQL应用》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 &#x1f680; 转载自热榜文章&#xff1a;设计模式深度解析&#xff1a;AI如何影响…...

【C语言环境】Sublime中运行C语言时MinGW环境的安装

要知道&#xff0c;GCC 官网提供的 GCC 编译器是无法直接安装到 Windows 平台上的&#xff0c;如果我们想在 Windows 平台使用 GCC 编译器&#xff0c;可以安装 GCC 的移植版本。 目前适用于 Windows 平台、受欢迎的 GCC 移植版主要有 2 种&#xff0c;分别为 MinGW 和 Cygwin…...

Ubuntu18.04 下Ublox F9P 实现RTK (利用CORS服务无需自建基站)

本内容参考如下连接:Ubuntu下Ublox F9P利用CORS服务无需自建基站实现RTK-CSDN博客 一、Ublox F9P 硬件模块示意图 图中展示了Ublox F9P的接口,包括串口2(`UART1`和`UART2`),USB1。需要人为通过u-center(Ublox F9P的显示软件)软件设置以下功能: Ublox通过`UART1`向PC端发送…...

springboot+vue在idea上面的使用小结

1.在mac上面删除java的jdk方法&#xff1a; sudo rm -rfjdk的路径 sudo rm -rf /Users/like/Library/Java/JavaVirtualMachines/corretto-17.0.10/Contents/Home 2.查询 Mac的jdk版本和路径&#xff1a; /usr/libexec/java_home -V 3.mac上面查询和关闭idea的网页端口&…...

MyEclipse将项目的开发环境与服务器的JDK 版本保持一致

前言 我们使用MyEclipse开发Java项目开发中&#xff0c;偶尔会遇到因项目开发环境不协调&#xff0c;导致这样那样的问题&#xff0c;在这里以把所有环境调整为JDK1.6 为例。 操作步骤 1.Window-->Preferences-->Java-->Installed JRES 修改为 1.6版本 2.Window-->…...

为BUG编程:函数重载的烦恼 char *匹配bool而不是string

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 这是一个BUG。 运行环境为linu…...

C++第十四弹---模板初阶

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、泛型编程 2、函数模板 2.1、函数模板的概念 2.2、函数模板的格式 2.3、函数模板的原理 2.4、函数模板的实例化 2.5、模板参数的匹配原则 …...

C++--内联函数

当调用一个函数时&#xff0c;程序就会跳转到该函数&#xff0c;函数执行完毕后&#xff0c;程序又返回到原来调用该函数的位置的下一句。 函数的调用也需要花时间&#xff0c;C中对于功能简单、规模小、使用频繁的函数&#xff0c;可以将其设置为内联函数。 内联函数&#xff…...

java数组与集合框架(一) -- 数据结构,数组

数据结构 概述 为什么要讲数据结构&#xff1f; 任何一个有志于从事IT领域的人员来说&#xff0c;数据结构&#xff08;Data Structure&#xff09;是一门和计算机硬件与软件都密切相关的学科&#xff0c;它的研究重点是在计算机的程序设计领域中探讨如何在计算机中组织和存储…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

【JavaEE】-- HTTP

1. HTTP是什么&#xff1f; HTTP&#xff08;全称为"超文本传输协议"&#xff09;是一种应用非常广泛的应用层协议&#xff0c;HTTP是基于TCP协议的一种应用层协议。 应用层协议&#xff1a;是计算机网络协议栈中最高层的协议&#xff0c;它定义了运行在不同主机上…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)

2025年能源电力系统与流体力学国际会议&#xff08;EPSFD 2025&#xff09;将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会&#xff0c;EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

FastAPI 教程:从入门到实践

FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Web 框架&#xff0c;用于构建 API&#xff0c;支持 Python 3.6。它基于标准 Python 类型提示&#xff0c;易于学习且功能强大。以下是一个完整的 FastAPI 入门教程&#xff0c;涵盖从环境搭建到创建并运行一个简单的…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

【决胜公务员考试】求职OMG——见面课测验1

2025最新版&#xff01;&#xff01;&#xff01;6.8截至答题&#xff0c;大家注意呀&#xff01; 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:&#xff08; B &#xff09; A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)

要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况&#xff0c;可以通过以下几种方式模拟或触发&#xff1a; 1. 增加CPU负载 运行大量计算密集型任务&#xff0c;例如&#xff1a; 使用多线程循环执行复杂计算&#xff08;如数学运算、加密解密等&#xff09;。运行图…...

Matlab | matlab常用命令总结

常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)

目录 一、&#x1f44b;&#x1f3fb;前言 二、&#x1f608;sinx波动的基本原理 三、&#x1f608;波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、&#x1f30a;波动优化…...

力扣-35.搜索插入位置

题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...