【数据结构】快排之三路划分+文件归并排序

news2025/3/13 2:58:30

排序

  • 一.快排
    • 1.快排性能分析
    • 2.快排之三路划分
    • 3.快排之内省排序
  • 二.归并
    • 1.外排序
    • 2.文件归并排序

一.快排

1.快排性能分析

决定快排性能的关键点是每次单趟排序后,key对数组的分割,如果每次选key基本二分居中,那么快排的递归树就是颗均匀的满二叉树,性能最佳。但是实践中虽然不可能每次都是二分居中,但是性能也还是可控的。如果出现每次选到最小值/最大值,划分为0个和N-1的子问题时,时间复杂度为O(N^2),数组序列有序时就会出现这样的问题,在《八大排序》已经用三数取中或者随机选key解决了这个问题,也就是说我们解决了绝大多数的问题,但是现在还是有一些场景没解决(数组中有大量重复数据时),类似以下数据。

//数组中有多个跟key相等的值
vector<int> a1 = { 6,1,7,6,6,6,4,9 };
vector<int> a2 = { 3,2,3,3,3,3,2,3 };

//数组中全是相同的值
vector<int> a3 = { 2,2,2,2,2,2,2,2 };

leetcode:排序数组

对于以上OJ题,传统的hoare和lomuto(前后指针法)的方法,过不了这个题目。堆排序和归并和希尔是可以过的,其它几个O(N^2)也不过的,因为这个题的测试用例中不仅仅有数据很多的大数组,也有一些特殊数据的数组,如大量重复数据的数组。堆排序和归并和希尔不是很受数据样本的分布和形态的影响,但是快排会,因为快排要选key,每次key都当趟分割都很偏,就会出现效率退化问题。

//hoare版本
class Solution 
{
public:
    void QuickSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return;

        int key = nums[left];
        int begin = left, end = right;

        while(begin < end)
        {
            while(begin < end && nums[end] >= key) end--;
            while(begin < end && nums[begin] <= key) begin++;
            swap(nums[begin], nums[end]);
        }
        swap(nums[left], nums[begin]);

        //递归左右区间
        //[left, begin - 1] [begin, end] [end + 1, right]
        QuickSort(nums, left, begin - 1);
        QuickSort(nums, end + 1, right);
    }

    vector<int> sortArray(vector<int>& nums) 
    {
        QuickSort(nums, 0, nums.size() - 1);
        return nums;
    }
};

在这里插入图片描述

//lomuto版本
class Solution 
{
public:
    void QuickSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return;

	    int keyi = left;
	    int prev = left, cur = left + 1;
	    while(cur <= right)
	    {
		    if(nums[cur] < nums[keyi])
		    {
			    prev++;
			    swap(nums[cur], nums[prev]);
		    }
		    cur++;
	    }
	    swap(nums[prev], nums[keyi]);
	    keyi = prev;

	    //递归左右区间
	    //[left, keyi - 1] keyi [keyi + 1, right]
	    QuickSort(nums, left, keyi - 1);
	    QuickSort(nums, keyi + 1, right);
    }

    vector<int> sortArray(vector<int>& nums) 
    {
        QuickSort(nums, 0, nums.size() - 1);
        return nums;
    }
};

在这里插入图片描述

从上面的的运行结果分析,无论是hoare,还是lomuto的前后指针法,面对key有大量重复时,划分都不是很理想。即使是三数取中和随机选key,都不能很好的解决这里的问题。但是三路划分算法,把跟key相等的值都划分到了中间,可以很好的解决这里的问题。

2.快排之三路划分

三路划分算法解析:当面对有大量跟key相同的值时,三路划分的核心思想有点类似hoare的左右指针和lomuto的前后指针的结合。核心思想是把数组中的数据分为三段【比key小的值】 【与key相等的值】【比key大的值】,所以叫做三路划分算法。结合下图,理解一下实现思想:

  1. key 默认取 left 位置的值。
  2. left 指向区间最左边,right 指向区间最后边,cur 指向 left+1 位置。
  3. cur 遇到比 key 小的值后跟 left 位置交换,换到左边,left++,cur++。
  4. cur 遇到比 key 大的值后跟 right 位置交换,换到右边,right–。
  5. cur 遇到跟 key 相等的值后,cur++。
  6. 直到 cur>right 结束。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution 
{
public:
    void QuickSort(vector<int>& nums, int left, int right)
    {
        if(left >= right) return;

        //随机选key
        int randi = left + (rand() % (right - left + 1)); //注意:加上left
        int key = nums[randi];
        swap(nums[left], nums[randi]);

        //三路划分
        int begin = left, end = right, cur = left + 1;
        while(cur <= end)
        {
            if(nums[cur] < key) swap(nums[begin++], nums[cur++]);
            else if(nums[cur] > key) swap(nums[cur], nums[end--]);
            else cur++;
        }

        //递归左右区间
        //[left, begin - 1] [begin, end] [end + 1, right]
        QuickSort(nums, left, begin - 1);
        QuickSort(nums, end + 1, right);
    }

    vector<int> sortArray(vector<int>& nums) 
    {
        QuickSort(nums, 0, nums.size() - 1);
        return nums;
    }
};

3.快排之内省排序

  1. C++STL中的sort中用的introspective sort(内省排序),introsort是由David Musser在1997年设计的排序算法
  2. introsort是introspective sort采用了缩写,它的实现思路就是进行自我侦测和反省,快排递归深度太深(SGI STL中使用的是深度为2倍排序元素数量的对数值)那就说明在这种数据序列下,选key出现了问题,性能在快速退化,那么就不要再进行快排分割递归了,改换为堆排序进行排序。

introsort的快排:

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//选出左右孩子中大的那一个
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//向下调整建堆:O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	//排序:O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[end], &a[0]);
		AdjustDown(a, end, 0);
		--end;
	}
}

void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		int end = i - 1;
		int tmp = a[i];
		//将tmp插入到[0, end]区间中,保持有序
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

void IntroSort(int* a, int left, int right, int depth, int defaultDepth)
{
	if (left >= right)
		return;

	//数组长度小于16的小数组,换为插入排序,简单递归次数
	if (right - left + 1 < 16)
	{
		InsertSort(a + left, right - left + 1);
		return;
	}

	//当深度超过2*logN时改用堆排序
	if (depth > defaultDepth)
	{
		HeapSort(a + left, right - left + 1);
		return;
	}
	depth++;
	int begin = left;
	int end = right;

	//随机选key
	int randi = left + (rand() % (right - left));
	Swap(&a[left], &a[randi]);
	int prev = left;
	int cur = prev + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}
	Swap(&a[prev], &a[keyi]);
	keyi = prev;

	//[begin, keyi-1] keyi [keyi+1, end]
	IntroSort(a, begin, keyi - 1, depth, defaultDepth);
	IntroSort(a, keyi + 1, end, depth, defaultDepth);
}

void QuickSort(int* a, int left, int right)
{
	int depth = 0;
	int logn = 0;
	int N = right - left + 1;
	for (int i = 1; i < N; i *= 2)
	{
		logn++;
	}

	//自省排序
	IntroSort(a, left, right, depth, logn * 2);
}

int* sortArray(int* nums, int numsSize, int* returnSize) 
{
	srand(time(0));
	QuickSort(nums, 0, numsSize - 1);
	*returnSize = numsSize;
	
	return nums;
}

二.归并

1.外排序

  1. 外排序(External sorting)是指能够处理极大量数据的排序算法。通常来说,外排序处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是一种“排序-归并”的策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。然后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。
  2. 跟外排序对应的就是内排序,我们之前讲的常见的排序,都是内排序,它们排序思想适应的是数据在内存中,支持随机访问。归并排序的思想不需要随机访问数据,只需要依次按序列读取数据,所以归并排序既是一个内排序,也是一个外排序。

在这里插入图片描述

2.文件归并排序

  1. 读取n个值排序后写到file1,再读取n个值排序后写到file2。
  2. file1和file2利用归并排序的思想,依次读取比较,取小的尾插到mfile,mfile归并为一个有序文件。
  3. 将file1和file2删掉,mfile重命名为file1。
  4. 再次读取n个数据排序后写到file2。
  5. 继续走file1和file2归并,重复步骤2,直到文件中无法读出数据。最后归并出的有序数据放到了file1中。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

//创建N个随机数,写到文件中
void CreateNDate()
{
	const int n = 10000000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; i++)
	{
		int x = rand() + i;
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

//qsort用的函数指针
int compare(const void* a, const void* b)
{
	return (*(int*)a - *(int*)b);
}

//读取数据后先排序再写到文件中,返回实际读取数据的个数,没有数据则返回0
int ReadNDataSortToFile(FILE* fout, int n, const char* file1)
{
	//开辟数组
	int x = 0;
	int* a = (int*)malloc(sizeof(int) * n);
	if (a == NULL)
	{
		perror("malloc error");
		return 0;
	}

	//读取N个数据到数组中,如果遇到文件结束,实际读到j个
	int j = 0;
	for (int i = 0; i < n; i++)
	{
		if (fscanf(fout, "%d", &x) == EOF)
			break;

		a[j++] = x;
	}

	if (j == 0)
	{
		free(a);
		return 0;
	}

	//内排序
	qsort(a, j, sizeof(int), compare);

	//创建文件
	FILE* fin = fopen(file1, "w");
	if (fin == NULL)
	{
		free(a);
		perror("fopen error");
		return 0;
	}

	//写排序后的数据到file1文件中
	for (int i = 0; i < j; i++)
	{
		fprintf(fin, "%d\n", a[i]);
	}

	free(a);
	fclose(fin);

	return j;
}

//将file1与file2中的数据归并到mfile文件中
void MergeFile(const char* file1, const char* file2, const char* mfile)
{
	FILE* fout1 = fopen(file1, "r");
	if (fout1 == NULL)
	{
		perror("fopen error");
		return;
	}

	FILE* fout2 = fopen(file2, "r");
	if (fout2 == NULL)
	{
		perror("fopen error");
		return;
	}

	FILE* mfin = fopen(mfile, "w");
	if (mfin == NULL)
	{
		perror("fopen error");
		return;
	}

	int x1 = 0, x2 = 0;
	int ret1 = fscanf(fout1, "%d", &x1);
	int ret2 = fscanf(fout2, "%d", &x2);

	while (ret1 != EOF && ret2 != EOF)
	{
		if (x1 < x2)
		{
			fprintf(mfin, "%d\n", x1);
			ret1 = fscanf(fout1, "%d", &x1);
		}
		else
		{
			fprintf(mfin, "%d\n", x2);
			ret2 = fscanf(fout2, "%d", &x2);
		}
	}
	while (ret1 != EOF)
	{
		fprintf(mfin, "%d\n", x1);
		ret1 = fscanf(fout1, "%d", &x1);
	}
	while (ret2 != EOF)
	{
		fprintf(mfin, "%d\n", x2);
		ret2 = fscanf(fout2, "%d", &x2);
	}

	fclose(fout1);
	fclose(fout2);
	fclose(mfin);
}

int main()
{
	CreateNDate();

	const char* file1 = "file1.txt";
	const char* file2 = "file2.txt";
	const char* mfile = "mfile.txt";

	FILE* fout = fopen("data.txt", "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return 0;
	}

	int m = 1000000;
	ReadNDataSortToFile(fout, m, file1);
	ReadNDataSortToFile(fout, m, file2);

	while (1)
	{
		MergeFile(file1, file2, mfile);

		//删除file1和file2
		remove(file1);
		remove(file2);

		//将mfile重命名为file1
		rename(mfile, file1);

		//继续读取数据,先排序,再写到file2中:若一个数据都读不到时,说明归并结束,结果在file1中
		if (ReadNDataSortToFile(fout, m, file2) == 0)
			break;
	}

	fclose(fout);

	return 0;
}

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

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

相关文章

机器学习笔记合集

&#x1f525;转载来源&#xff1a;机器学习笔记合集 大家好&#xff0c;这里是好评笔记&#xff0c;公主 号&#xff1a;Goodnote。本笔记的任务是解读机器学习实践/面试过程中可能会用到的知识点&#xff0c;内容通俗易懂&#xff0c;入门、实习和校招轻松搞定。 笔记介绍 本…

2025年01月15日Github流行趋势

1. 项目名称&#xff1a;tabby - 项目地址url&#xff1a;https://github.com/TabbyML/tabby - 项目语言&#xff1a;Rust - 历史star数&#xff1a;25764 - 今日star数&#xff1a;1032 - 项目维护者&#xff1a;wsxiaoys, apps/autofix-ci, icycodes, liangfung, boxbeam - 项…

晨辉面试抽签和评分管理系统之九:随机编排考生的分组(以教师资格考试面试为例)

晨辉面试抽签和评分管理系统&#xff08;下载地址:www.chenhuisoft.cn&#xff09;是公务员招录面试、教师资格考试面试、企业招录面试等各类面试通用的考生编排、考生入场抽签、候考室倒计时管理、面试考官抽签、面试评分记录和成绩核算的面试全流程信息化管理软件。提供了考生…

Mongodb相关内容

Mongodb相关内容 1、Windows平台安装2、Linux平台安装3、基本常用命令文档更新删除文档分页查询索引 pymongo操作 客户端下载&#xff1a;https://download.csdn.net/download/guoqingru0311/90273435 1、Windows平台安装 方式一&#xff1a; 方式2&#xff1a; 方式3&#…

Vue3使用vue-count-to数字滚动模块报错解决方案

小伙伴们是不是遇到了vue3项目使用vue-count-to出现报错的问题 报错如下&#xff1a; TypeError: Cannot read properties of undefined (reading _c) 这个错误信息具体是说没读取到_c的属性 具体不清楚是什么原因&#xff0c;排查还得去看源码&#xff0c;所以我们来解决&a…

C#,图论与图算法,输出无向图“欧拉路径”的弗勒里(Fleury Algorithm)算法和源程序

1 欧拉路径 欧拉路径是图中每一条边只访问一次的路径。欧拉回路是在同一顶点上开始和结束的欧拉路径。 这里展示一种输出欧拉路径或回路的算法。 以下是Fleury用于打印欧拉轨迹或循环的算法&#xff08;源&#xff09;。 1、确保图形有0个或2个奇数顶点。2、如果有0个奇数顶…

H3CNE-12-静态路由(一)

静态路由应用场景&#xff1a; 静态路由是指由管理员手动配置和维护的路由 路由表&#xff1a;路由器用来妆发数据包的一张“地图” 查看命令&#xff1a; dis ip routing-table 直连路由&#xff1a;接口配置好IP地址并UP后自动生成的路由 静态路由配置&#xff1a; ip…

【2024年华为OD机试】 (C卷,100分)- 数字涂色(Java JS PythonC/C++)

一、问题描述 题目描述 疫情过后&#xff0c;希望小学终于又重新开学了&#xff0c;三年二班开学第一天的任务是将后面的黑板报重新制作。 黑板上已经写上了N个正整数&#xff0c;同学们需要给这每个数分别上一种颜色。 为了让黑板报既美观又有学习意义&#xff0c;老师要求…

JavaScript动态渲染页面爬取之Splash

Splash是一个 JavaScript渲染服务,是一个含有 HTTP API的轻量级浏览器,它还对接了 Python 中的 Twisted 库和 OT库。利用它&#xff0c;同样可以爬取动态渲染的页面。 功能介绍 利用 Splash&#xff0c;可以实现如下功能&#xff1a; 异步处理多个网页的渲染过程:获取渲染后…

天机学堂2-高并发优化

day04-高并发优化 方案选择 实现了学习计划和学习进度的统计功能。特别是学习进度部分&#xff0c;为了更精确的记录用户上一次播放的进度&#xff0c;我们采用的方案是&#xff1a;前端每隔15秒就发起一次请求&#xff0c;将播放记录写入数据库。 在并发较高的情况下&#xf…

ROS2 准备工作(虚拟机安装,Ubuntu安装,ROS2系统安装)

准备工作 虚拟机安装 大家可以自行去安装VMware链接&#xff1a;https://pan.baidu.com/s/1KcN1I9FN--Sp1bUsjKqWVA?pwd6666 提取码&#xff1a;6666(提供者&#xff1a;零基础编程入门教程) 教程&#xff1a;【【2025最新版】VMware虚拟机安装教程&#xff0c;手把手教你免…

在一个地方待多久才会改变ip属地

‌在当今数字化时代&#xff0c;IP地址作为网络世界的“门牌号”&#xff0c;不仅承载着设备连接互联网的身份信息&#xff0c;还常常与地理位置相关联。随着人们频繁地迁徙、旅行或在不同地点工作&#xff0c;一个自然而然的问题浮现在许多人心头&#xff1a;究竟在一个地方待…

CCLINKIE转ModbusTCP网关,助机器人“掀起”工业智能的“惊涛骇浪”

以下是一个稳联技术CCLINKIE转ModbusTCP网关&#xff08;WL-CCL-MTCP&#xff09;连接三菱PLC与机器人的配置案例&#xff1a;设备与软件准备设备&#xff1a;稳联技术WL-CCL-MTCP网关、三菱FX5UPLC、支持ModbusTCP协议的机器人、网线等。 稳联技术ModbusTCP转CCLINKIE网关&…

QT在 MacOS X上,如何检测点击程序坞中的Dock图标

最近在开发MacOS的qt应用&#xff0c;在做到最小化系统托盘功能时&#xff0c;发现关闭窗口后再次点击程序坞中的Dock图标不能将主界面再显示出来。查询里很多资料&#xff0c;发现是QT自身的问题&#xff0c;没有做相关的点击Dock图标的处理。 于是我参考了国内和国外的这两篇…

langchain4j执行源码分析

要做大模型应用&#xff0c;不可避免会接触到langchain&#xff0c;但是langchain本身使用py实现&#xff0c;对于java用户上手体验不是很友好。继而出现了java版的langchain&#xff0c;即langchain-4j。这里我们用脑图分析一下其执行源码。

【案例81】NMC调用导致数据库的效率问题

问题现象 客户在使用NC系统时&#xff0c;发现系统特别卡顿。需要紧急排查。 问题分析 排查NMC发现&#xff0c;所有的线程都处于执行SQL层面&#xff0c;说明数据库当前出现了异常。查看数据库资源状态发现&#xff0c;Oracle相关进程CPU利用率达到了100%。 查看现在数据库…

PyTorch框架——基于深度学习YOLOv5神经网络水果蔬菜检测识别系统

基于深度学习YOLOv5神经网络水果蔬菜检测识别系统&#xff0c;其能识别的水果蔬菜有15种&#xff0c;# 水果的种类 names: [黑葡萄, 绿葡萄, 樱桃, 西瓜, 龙眼, 香蕉, 芒果, 菠萝, 柚子, 草莓, 苹果, 柑橘, 火龙果, 梨子, 花生, 黄瓜, 土豆, 大蒜, 茄子, 白萝卜, 辣椒, 胡萝卜,…

DFT可测性设置与Tetramax测试笔记

1 DFT 1.1 DFT类型 1、扫描链&#xff08;SCAN&#xff09;&#xff1a; 扫描路径法是一种针对时序电路芯片的DFT方案.其基本原理是时序电路可以模型化为一个组合电路网络和带触发器(Flip-Flop&#xff0c;简称FF)的时序电路网络的反馈。 Scan 包括两个步骤&#xff0c;scan…

分布式ID的实现方案

1. 什么是分布式ID ​ 对于低访问量的系统来说&#xff0c;无需对数据库进行分库分表&#xff0c;单库单表完全可以应对&#xff0c;但是随着系统访问量的上升&#xff0c;单表单库的访问压力逐渐增大&#xff0c;这时候就需要采用分库分表的方案&#xff0c;来缓解压力。 ​…

28.找出字符串中第一个匹配项的下标【力扣】KMP前缀表 ≈ find() 函数、暴力解法

class Solution { public: //得到前缀表void getNext(int *next,string needle){int j0;for(int i1;i<needle.size();i){while(j>0 && needle[j]!needle[i]) jnext[j-1];//**j>0**>j0是出口if(needle[i]needle[j]) j;next[i]j;//若写入if中&#xff0c;则该…