【数据结构】归并排序和计数排序(排序的总结)

news2024/9/30 21:29:29

目录

一,归并排序的递归

二,归并排序的非递归

三,计数排序

四,排序算法的综合分析


一,归并排序的递归

基本思想:

        归并采用的是分治思想,是分治法的一个经典的运用。该算法先将原数据进行拆分,此步骤与二叉树的拆分思想一样(因此,运用递归比较简单),然后将最终拆分后的每一小部分排序,最后将已有序的子序列进行合并,得到完全有序的序列,其中关键为要使每个分割后的子序列有序,再使子序列段间有序,即合并有序序列。以上中将两个有序表合并成一个有序表称为二路归并。思想图如下(以升序为例):

        上图中,先以中间数据为界,将一堆数据进行不断分解,当分解完全后,再进行合并,而在合并时其实就是边排序边合并。由于在排序中要改动原数据,因此,我们可再创建一个数组进行改动,然后将改动后的数据赋值给原数据块即可,代码和导图如下:

代码运行导图

        导图中,先取中间值,以此下标为界限分开左右区间,然后再不断递归分割,最后一次分割为leftbegin == leftend,rightbegin == rightend,此时就要进行排序组合,组合完子序列后即可往原序列就行赋值,此为一趟遍历,然后递归就不断进行返回,即不断就行合并排序,最终全部元素有序。

归并代码:

void MergeFunction(int* a, int* nums, int n, int begin, int end) {
    //当分割区间为1个数据时就要停止分割,即此时begin == end
    if (begin == end) {
        return;
    }
    int middle = (begin + end) / 2;//中间数据,控制界限
    //在左区间[begin, middle]和右区间[middle + 1, end]进行不断分割
    MergeFunction(a, nums, n, begin, middle);
    MergeFunction(a, nums, n, middle + 1, end);
    //分割后,下面是进行左右区间的排序
    int leftbegin = begin, leftend = middle;
    int rightbegin = middle + 1, rightend = end;
    int insert = begin;
    //以下是进行分割后的排序
    while (leftbegin <= leftend && rightbegin <= rightend) {
        if (a[leftbegin] < a[rightbegin]) {
            nums[insert++] = a[leftbegin++];
        }
        else {
            nums[insert++] = a[rightbegin++];
        }
    }
    while (leftbegin <= leftend) {
        nums[insert++] = a[leftbegin++];
    }
    while (rightbegin <= rightend) {
        nums[insert++] = a[rightbegin++];
    }
    //拷贝数组
    memcpy(a + begin, nums + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n) {
    int* nums = (int*)malloc(sizeof(int) * n);//此数组用于临时放入数据
    if (!nums) {
        perror("nums malloc");
        exit(-1);
    }
    MergeFunction(a, nums, n, 0, n - 1);
    free(nums);
}

样例代码,将以下中数组a进行排序(升序):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void MergeFunction(int* a, int* nums, int n, int begin, int end) {
	//当分割为1个时就要停止分割,即此时begin == end
	if (begin == end) {
		return;
	}
	int middle = (begin + end) / 2;//中间数据,控制界限
	//在左区间[begin, middle]和右区间[middle + 1, end]进行不断分割
	MergeFunction(a, nums, n, begin, middle);
	MergeFunction(a, nums, n, middle + 1, end);
	//分割后,下面是进行左右区间的排序
	int leftbegin = begin, leftend = middle;
	int rightbegin = middle + 1, rightend = end;
	int insert = begin;
	//以下是进行分割后的排序
	while (leftbegin <= leftend && rightbegin <= rightend) {
		if (a[leftbegin] < a[rightbegin]) {
			nums[insert++] = a[leftbegin++];
		}
		else {
			nums[insert++] = a[rightbegin++];
		}
	}
	while (leftbegin <= leftend) {
		nums[insert++] = a[leftbegin++];
	}
	while (rightbegin <= rightend) {
		nums[insert++] = a[rightbegin++];
	}
	//拷贝数组
	memcpy(a + begin, nums + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n) {
	int* nums = (int*)malloc(sizeof(int) * n);//此数组用于临时放入数据
	if (!nums) {
		perror("nums malloc");
		exit(-1);
	}
	MergeFunction(a, nums, n, 0, n - 1);
	free(nums);
}
int main() {
	int a[] = { 10,6,7,1,3,9,4,2 };
	MergeSort(a, sizeof(a) / sizeof(int));
	for (int i = 0; i < sizeof(a) / sizeof(int); i++) {
		fprintf(stdout, "%d ", a[i]);
	}
	puts("");
	return 0;
}

运行图:


二,归并排序的非递归

        我们平常将递归改成非递归首先可能想起要运用栈结构,但是,我们先理一下归并的思路,当我们不断分割时确实可以用栈结构来控制区间,但是当回并时可能就比较麻烦,因此,本人不建议用栈结构,不是不可以,是有更好的方法。

        归并递归时是将数据不断进行二分,即分治思想,当用非递归时,我们可设置一个间隔gap,以次模仿递归时的二分思想,每次循环结束后将此间隔乘二即可。非递归思路导图如下:

        由以上图不难发现,此种非递归合并的思路与递归合并的思路有些不太一样,此种非递归的思想是一旦有了一个间隔值后,就一次性的将全部数据按照此间隔值进行间隔归并。

        非递归归并的时候要注意一个点,当数据个数为奇数时,不难发现,左区间[leftbegin,leftend]无影响,但右区间[rightbegin,rightend]将会溢出,此时情况,如果rightbegin溢出的话那么可之间退出,因为据上图中所示,每一趟归并时就相当于将下一趟的左区间就排列有序了;如果rightend溢出的话,直接令rightend为最后一个元素的下标进行归并排序即可。当数据个数为偶数时不会出现溢出情况。

代码如下:

void MergeSortNonR(int* a, int n) {
    int* nums = (int*)malloc(sizeof(int) * n);
    //gap是每次隔离的间隔,也可理解为将要排序的元素个数
    int gap = 1;

    //控制gap间据的大小
    while (gap < n) {
        for (int i = 0; i < n; i += 2 * gap) {
            //以gap为间距分割,左区间[leftbegin,leftend]共有gap个元素
            int leftbegin = i, leftend = i + gap - 1;
            //以gap为间距分割,当原始数有偶数的元素时,右区间[rightbegin,rightend]有gap个元素
            int rightbegin = i + gap, rightend = i + gap + gap - 1;
            int insert = i;
            //以下是当元素个数为奇数时的情况
            if (rightbegin >= n) {
                break;
            }
            if (rightend >= n) {
                rightend = n - 1;
            }
            //开始进行排序,排列的数据区间为[leftbegin, rightend]
            while (leftbegin <= leftend && rightbegin <= rightend) {
                if (a[leftbegin] < a[rightbegin]) {
                    nums[insert++] = a[leftbegin++];
                }
                else {
                    nums[insert++] = a[rightbegin++];
                }
            }
            while (leftbegin <= leftend) {
                nums[insert++] = a[leftbegin++];
            }
            while (rightbegin <= rightend) {
                nums[insert++] = a[rightbegin++];
            }
            //将排列的区间[leftbegin, rightend]进行拷贝,因为leftbegin和rightbegin都已改变,所以不能用这两个数据
            memcpy(a + i, nums + i, sizeof(int) * (rightend - i + 1));
        }
        gap *= 2;
    }
    free(nums);
}

样例代码,将以下中数组a进行排序(升序):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void MergeSortNonR(int* a, int n) {
	int* nums = (int*)malloc(sizeof(int) * n);
	//gap是每次隔离的间隔,也可理解为将要排序的元素个数
	int gap = 1;
	while (gap < n) {
		for (int i = 0; i < n; i += 2 * gap) {
			//leftbegin和leftend理解为在gap区间内的元素
			int leftbegin = i, leftend = i + gap - 1;
			//rightbegin和rightend可理解为在预排序gap区间的后面的元素
			int rightbegin = i + gap, rightend = i + gap + gap - 1;
			int insert = i;
			//以下是当元素为奇数时的情况
			if (rightbegin >= n) {
				break;
			}
			if (rightend >= n) {
				rightend = n - 1;
			}
			//开始进行排序,排列的数据区间为[leftbegin, rightend]
			while (leftbegin <= leftend && rightbegin <= rightend) {
				if (a[leftbegin] < a[rightbegin]) {
					nums[insert++] = a[leftbegin++];
				}
				else {
					nums[insert++] = a[rightbegin++];
				}
			}
			while (leftbegin <= leftend) {
				nums[insert++] = a[leftbegin++];
			}
			while (rightbegin <= rightend) {
				nums[insert++] = a[rightbegin++];
			}
			//将排列的区间[leftbegin, rightend]进行拷贝,因为leftbegin和rightbegin都以改变,所以不能用这两个数据
			memcpy(a + i, nums + i, sizeof(int) * (rightend - i + 1));
		}
		gap *= 2;
	}
	free(nums);
}
int main() {
	int a[] = { 10,6,7,1,3,9,4,2 };
	MergeSortNonR(a, sizeof(a) / sizeof(int));
	for (int i = 0; i < sizeof(a) / sizeof(int); i++) {
		fprintf(stdout, "%d ", a[i]);
	}
	puts("");
	return 0;
}

运行图:


运算发的效率:

        最后我们来谈论一下此算法的效率,不难发现,由于此算法中运用的是二分思想,所以时间复杂度为O(nlogn),空间复杂度O(n)。由此可见,此算法的效率也算高,跟快排不同的是,此算法与原始数据的初始位置并无太大影响,效率比较稳定。


三,计数排序

        计数排序可从字面意思理解,通过数组的下标来计数来间接实现数据的排序。首先,我们需设置一个数组,此数组是通过下标来进行计数的,原数据中最大数据个数为max - min + 1(max是原数据的最大元素,min是原数据最小的元素),即幅度range在区间[0,max - min]中,因此,我们可设置数组大小为max - min + 1(不包括重复数组,重复的数据我们可用相同下标对应的数值记录出现的次数),即最大下标为max - min(记录最大元素的位置),下标的设置思想为:原数据 - min。然后将设置数组初始化为0(也可选举其它值,但选举0是最为简单的),0表示原数据中没有此元素,然后幅度range遍历,一旦存在此元素加1,表示出现元素的次数,最后将其设置数组中出现过元素的下标加上min赋给原数据即可得到有序序列,思维导图如下:


代码如下(以升序为例):

void CountSort(int* a, int n) {
    //以下是寻找最大值max和最小值min,为了后面确定幅度range
    int min = a[0], max = a[n - 1], j = 0;
    for (int i = 0; i < n; i++) {
        if (a[i] > max) {
            max = a[i];
        }
        if (a[i] < min) {
            min = a[i];
        }
    }
    //最大元素个数为range,下标为“原数据 - min”
    int range = max - min + 1;
    int* count = (int*)malloc(sizeof(int) * range);
    memset(count, 0, sizeof(int) * range);//初始化0,表元素个数为0
    for (int i = 0; i < n; i++) {
        //用计数数组count的有序下标来进行有序计数,记录出现过元素的次数
        count[a[i] - min]++;//注意: 不能count[a[i] - min] = 1,因为可能有重复数据
    }
    //最后遍历,一旦存在此值将,下标 + min即为数据
    for (int i = 0; i < range; i++) {
        while (count[i]--) {
            a[j++] = i + min;
        }
    }
    free(count);
}

代码演示(以升序为例):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void CountSort(int* a, int n) {
	//以下是寻找最大值max和最小值min,为了后面确定幅度range
	int min = a[0], max = a[n - 1], j = 0;
	for (int i = 0; i < n; i++) {
		if (a[i] > max) {
			max = a[i];
		}
		if (a[i] < min) {
			min = a[i];
		}
	}
	//最大元素个数为range,下标为“原数据 - min”
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	memset(count, 0, sizeof(int) * range);//初始化0,表元素个数为0
	for (int i = 0; i < n; i++) {
		//用计数数组count的有序下标来进行有序计数,记录出现过元素的次数
		count[a[i] - min]++;//注意: 不能count[a[i] - min] = 1,因为可能有重复数据
	}
	//最后遍历,一旦存在此值将,下标 + min即为数据
	for (int i = 0; i < range; i++) {
		while (count[i]--) {
			a[j++] = i + min;
		}
	}
	free(count);
}
int main() {
	int a[] = { 10,6,7,1,3,9,4,2 };
	CountSort(a, sizeof(a) / sizeof(int));
	for (int i = 0; i < sizeof(a) / sizeof(int); i++) {
		fprintf(stdout, "%d ", a[i]);
	}
	puts("");
	return 0;
}

运行图:


计数效率与注意要点:

        计数排序的时间复杂度为O(range + n) ,空间复杂度为O(range),效率是非常高的,因为无论什么排序算法,最好的时间效率无非是O(n),而计数排序的时间复杂度达到O(range + n),已经非常接近O(n)。无论是希尔排序,堆排序,快速排序还是归并排序都达不到此效率,但此算法也不是最优选择,因为此算法完全可以说是那空间换时间,当range非常大时会消耗很大的空间,而且由于此算法是运用数组下标进行间接排序的,因此,此算法只能对整型排序,不能对其它数据类型进行排序,使得此算法有了很大的局限性。


四,排序算法的综合分析

1,算法的效率分析

        排序算法的效率不能只根据时间复杂度和空间复杂度,因为两者的计算都是取最好情况和最坏情况进行概率的综合分析,最终取平均,比如排序的原始序列不同,快排算法,直接插入算法和冒泡算法的效率比较大,直接插入和冒泡最好的情况都是有序的情况,此时时间复杂度都为O(n),而快排最好的情况是基准值每次在中间,此时时间复杂度为O(nlogn)。但大多数情况下,我们根本预测不了原始序列和原始数据,所以在大多数情况下可直接根据时间复杂度和空间复杂度直接判断,若对数据比较敏感的话就要根据具体情况进行具体分析,从而选取最优算法。

2,算法的稳定性

        稳定性:相同的数据排序后,相对位置是否发生变化,若两者之间的相对位置没有发生变化,则算法稳定,发生变化,算法不稳定。

        在初学情况下,稳定性确实不算太重要,但是在后面深入学习系统操作和程序等先后顺序就显得尤为重要,例如,一个程序要对学生进行考试排名,其中高成绩出现了两个99分的和两个98分,这时两个99分的学生谁先排入第一名和两个98分的学生谁排入第三名就显得尤为重要,这就要求排序算法的稳定性。最后总结一下各个算法的效率和稳定性:

        其中计数排序没必要加上,因为计数局限性太强了,在后面的学习中基本用不到,并且提醒一下,以上中的算法效率和稳定性千万不要死记,因为之前说过,这些算法的效率基本都不稳定,不同的情况可能出现不同的效率,而某些算法的设计不同可能稳定,也可能不稳定,因此,理解算法的思想和如何实现尤为重要。

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

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

相关文章

what?es数据偏移了8小时...

今天搞监控大屏的时候&#xff0c;测试突然提出一个问题说&#xff0c;查一段时间的数据&#xff0c;时间曲线返回的日期有时候会比查询时间多&#xff0c;翻看代码后&#xff0c;初步定位为es的时区问题&#xff0c;后来将时间曲线的直方图聚合增加时区后&#xff0c;返回数据…

android studio 、JDK环境变量配置

1、adb.exe环境变量配置&#xff1a; 打开控制面板 >系统和安全>系统>高级系统设置 在系统变量中新建ANDROID_HOME变量&#xff0c;赋值路径&#xff1a;D:\install\androidSDK 在系统变量path中添加&#xff1a;%ANDROID_HOME%\platform-tools 校验是…

14:00面试,14:06就出来了,这面试问的过于变态了。。。

前言 刚从小厂出来&#xff0c;没想到在另一家公司我又寄了。 在这家公司上班&#xff0c;每天都要加班&#xff0c;但看在钱给的比较多的份上&#xff0c;也就不太计较了。但万万没想到十月一纸通知&#xff0c;所有人不准加班了&#xff0c;不仅加班费没有了&#xff0c;薪资…

什么是事件对象(event object)?如何使用它获取事件信息?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

分类预测 | MATLAB实现KOA-CNN-LSTM开普勒算法优化卷积长短期记忆神经网络数据分类预测

分类预测 | MATLAB实现KOA-CNN-LSTM开普勒算法优化卷积长短期记忆神经网络数据分类预测 目录 分类预测 | MATLAB实现KOA-CNN-LSTM开普勒算法优化卷积长短期记忆神经网络数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.MATLAB实现KOA-CNN-LSTM开普勒算法优化…

NFTScan | 10.02~10.08 NFT 市场热点汇总

欢迎来到由 NFT 基础设施 NFTScan 出品的 NFT 生态热点事件每周汇总。 周期&#xff1a;2023.10.02~ 2023.10.08 NFT Hot News 01/ 9 月 OpenSea 交易额为 7300 万美元&#xff0c;创两年新低 10 月 2 日&#xff0c;数据显示 9 月 NFT 平台 OpenSea 的交易总额为 73,141,407…

Pyside6 QPushButton

Pyside6 QPushButton QPushButton使用QPushButton继承关系QPushButton的函数(Function)和信号(Signal)QPushButton信号 QPushButton例程界面设计clicked信号测试pressed信号测试released信号测试toggled信号测试按键长按测试按键长按间隔测试完整程序界面程序主程序 按键或命令…

2023年【陕西省安全员C证】新版试题及陕西省安全员C证考试试卷

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 陕西省安全员C证新版试题是安全生产模拟考试一点通总题库中生成的一套陕西省安全员C证考试试卷&#xff0c;安全生产模拟考试一点通上陕西省安全员C证作业手机同步练习。2023年【陕西省安全员C证】新版试题及陕西省安…

Idea本地跑flink任务时,总是重复消费kafka的数据(kafka->mysql)

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 Idea中执行任务时&#xff0c;没法看到JobManager的错误&#xff0c;以至于我以为是什么特殊的原因导致任务总是反复消费。在close方法中&#xff0c;增加日志&#xff0c;发现jdbc连接被关闭了。 重新…

通讯网关软件019——利用CommGate X2OPCUA实现OPC UA访问Oracle服务器

本文介绍利用CommGate X2OPCUA实现OPC UA访问ORACLE数据库。CommGate X2OPCUA是宁波科安网信开发的网关软件&#xff0c;软件可以登录到网信智汇(http://wangxinzhihui.com)下载。 【案例】如下图所示&#xff0c;实现上位机通过OPC UA来获取ORACLE数据库的数据。 【解决方案】…

windows docker desktop配置加速地址

目录 为什么常见加速地址在docker desktop上配置 为什么 https://hub.docker.com 是官方的镜像仓库地址&#xff0c;但是它的服务器地址是在国外&#xff0c;有时候访问和下载的速度差强人意。不过好在&#xff0c;我们可以进行远程仓库的设置&#xff0c;将仓库镜像地址设置为…

2023,全网最真实的自动化测试学习路线,看不懂来打我!

随着测试行业的发展&#xff0c;“会代码”越来越成为测试工程师的一个标签。打开各大招聘网站&#xff0c;测试工程师月薪一万以上基本都有一个必备技能&#xff0c;那就是自动化测试。那么自动化测试到底难不难呢&#xff1f;下面我将会将我的经历讲给大家听&#xff0c;希望…

1556. 千位分隔数

1556. 千位分隔数 C代码&#xff1a; char * thousandSeparator(int n){char* str (char*)malloc(sizeof(char) * 20);int len sprintf(str, "%d", n);int len2 0;if (len % 3 0) {len2 len len / 3 - 1;} else {len2 len len / 3;}char* ans (char*)malloc…

FTP服务器搭建

操作系统系列文章 VMware Workstation Player 17 免费下载安装教程 VMware Workstation 17 Pro 免费下载安装教程 windows server 2012安装教程 Ubuntu22.04.3安装教程 FTP服务器搭建 FTP服务器搭建教程 操作系统系列文章前言基本概念介绍一、安装FTP服务二、配置ftp服务三、建…

androidStudio第一次运行报错无法运行

安卓第一次运行失败 大家好&#xff0c;我使用androidStudio新建了一个测试demo第一次运行&#xff0c;结果失败了&#xff0c;显示如下图&#xff1a; 然后查了各种方法&#xff0c;都是没有用&#xff0c;最后 历经困难&#xff0c;还是找到了&#xff0c;原来是 gradle的依…

电子招标投标系统 —采购招投标管理一体化系统-

项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以及审…

Visual Studio Code配置C/C++开发环境

C/C开发中的IDE非常多&#xff0c;网上有推荐安装Visual Studio 2019/2020/2022。但是登录官方网址下载&#xff0c;此软件体积非常大(8G以上)&#xff0c;且企业版、专业版会收费。 因此&#xff0c;我们推荐大家可以尝试通过Visual Studio Code来配置C/C开发环境 环境准备 Mi…

WorkPlus定制化的局域网会议软件,提供安全稳定的会议体验

在现代商业环境中&#xff0c;迅速而高效的沟通是企业成功的关键要素之一。而在传统的会议模式下&#xff0c;时间成本和地理限制往往给企业带来不小的困扰。针对这一问题&#xff0c;WorkPlus推出了一款创新的局域网会议软件——WorkPlus Meet&#xff0c;旨在为企业创造高效的…

掌握核心技巧就能创建完美的目录!如何在Word中自动创建目录

目录是Word布局的一个重要因素&#xff0c;尤其是在编写较长的文档时。那么&#xff0c;你如何在你的作品中添加目录呢&#xff1f;在这篇文章中&#xff0c;我将分享一些基于Word2016自动创建目录的经验。希望它能或多或少地帮到你。 自动创建目录 1、输入目录文本的名称&am…

前端页面布局之【Flex布局】详解

目录 &#x1f31f;前言&#x1f31f;浏览器支持&#x1f31f;Flex简介&#x1f31f;Flex基本概念&#x1f31f;容器属性&#x1f31f;项目排列方向&#x1f31f;项目包裹方式&#x1f31f;项目水平对齐方式&#x1f31f;项目的垂直对齐方式&#x1f31f;多行对齐方式 &#x1…