三大排序—六种算法

三大排序—六种算法

前言

在日常生活中,排序是我们会经常遇到的问题。而学习数据结构与算法,排序也是绕不过去的坎。这里介绍了插入排序,选择排序,交换排序,以及基于这三种排序的改良版。和我一起来学习这三种排序方法吧。

一、插入排序

1、算法简介

先默认下标为0的数值最小,从下标为1的地方开始,若发现当前数值比前面的数值tmp小,则插入到这个数值tmp之前。形象一点来说,就是像小时候排队。先随便找一个孩子,让他成为队首,然后第二个小朋友和队首比较身高,如果比队首高,就站在队首后面,若比队首矮,则站在队首的前面,成为新队首。然后第三个孩子继续从队首开始比较,找到比他高的,就站在比他高的孩子的前面,没有就站到队尾。依次类推,直到所有的孩子排完队。

在这里插入图片描述

这就像在找自己位置一样,不是就往后继续找,找到了就坐在那!先给出代码。

void insertSort(int *data, int count) {
	int i;
	int j;
	int t;
	int tmp;

	for (i = 1; i < count; i++) {
		tmp = data[i];//先把找座位的保护起来
		for (j = 0; j < i && tmp >= data[j]; j++) {
		}//找到座位
		for (t = i; t > j; t--) {
			data[t] = data[t - 1];
		}//让座位后面的人向后移动,像不像插队呢-_-
		data[j] = tmp;//让这个人做到座位上
	}	
}

2、算法分析

在这里插入图片描述

二、希尔排序(改良后的插入排序)

1、算法简介

希尔排序是基于插入排序的改良版之一。由于插入排序对有序数据的排序速度快,希尔排序就是先将数据变得基本有序。

基本思想是将n个数据逐次分为两半。就是先设置一个步长n/2。然后每个数据和这个数据+步长的数据进行插入排序。循环结束,步长/2…直到步长为1。

在这里插入图片描述

就是像跳跃一般,每次和相等步长的数进行比较,这样不但会将比较小的放在前面,还会减少了许多的移动空间的操作。

void ShellSort(int *data, int count) {
	int start;
	int step;

    //外循环控制步长长度,内循环从0到步长的位置
	for (step = count/2; step; step /= 2) {
		for (start = 0; start < step; start++) {
			shellOneSort(data, count, start, step);
		}
	}
}

//希尔排序的一步,原型就是插入排序
void shellOneSort(int *data, int count, int start, int step) {
	int i;
	int j;
	int t;
	int tmp;
	
	for (i = start + step; i < count; i += step) {
		tmp = data[i];
		for (j = start; j < i && tmp >= data[j]; j += step) {
		}
		for (t = i; t > j; t -= step) {
			data[t] = data[t - step];
		}
		data[j] = tmp;
	}	
}

2、算法分析

最差情况也是O(n*n),即完全倒序,比较和赋值与插入排序相同。

由于计算希尔排序的时间复杂度,涉及现在还未解决的数学难题。所以这里只给出他的时间复杂度。

希尔排序的时间复杂度是:O(nlogn)~O(n2),平均时间复杂度大致是O(1.5n)。

三、选择排序

1、算法简介

从第一个数据开始,找所有数据中的最小值,若与第一个数据不同,则这两个数据进行交换。然后再从第二个数据开始……

选择排序的原理是比较简单的。找最小数、交换等等。

在这里插入图片描述

代码实现:

void selectSort(int *data, int count) {
	int i;
	int j;
	int minIndex;
	int tmp;

	for (i = 0; i < count - 1; i++) {
		minIndex = i;
        //寻找最小数下标
		for (j = i; j < count; j++) {
			if (data[j] < data[minIndex]) {
				minIndex = j;
			}
		}
        //最小下标不等于minIndex时,进行交换。
		if (minIndex != i) {
			tmp = data[i];
			data[i] = data[minIndex];
			data[minIndex] = tmp;
		}
	}	
}

2、算法分析

从代码分析,无论如何,都会进行n*(n - 1)次比较,所以没有高潮也没有低潮,那么这里的交换就成了相对于比较而言比较重要的点。

最优情况:完全顺序,只比较,不交换

最差情况:完全逆序,比较且交换。

综上所述:

时间复杂度为O(n*n)

空间复杂度为O(1)

四、堆排序(改良后的选择排序)

1、算法简介

堆排序是基于大根堆(升序)和小根堆(降序)

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

下面图片来自https://www.cnblogs.com/chengxiao/p/6129630.html

img

实际上,我们可以将数组看作一个完全二叉树,实际上也确实如此。

每次生成大根堆时,从完全二叉树的最后一个叶子节点开始,对一个父节点进行生成大根堆的处理。然后从最后一父节点开始,依次对每个父节点做相似处理。最后会使得最大元素恰好就是根节点,再将根节点于最后一个叶子节点进行交换。下次的生成大根堆时,不把排序好的数据计入大根堆。

在这里插入图片描述

看完图再简单总结下堆排序的基本思路:

a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

代码实现:

void adjustHeap(int *data, int count, int root) {//处理一个根下面的节点及根节点。使之成为一个大根堆
	int left;
	int right;
	int maxNode;
	int tmp;

	while (root <= count / 2 - 1) {
		left = 2*root + 1;
		right = left + 1;

		maxNode = right >= count ? left
				: (data[left] > data[right] 
				? left : right);//确定根节点是否存在右孩子,以及左右孩子的最大点
		maxNode = data[root] > data[maxNode]
				? root : maxNode;//比较根节点与左右孩子中的最大点的大小
		if (maxNode == root) {//若最大点已经是根节点,直接返回即可
			return;
		}
		tmp = data[root];
		data[root] = data[maxNode];
		data[maxNode] = tmp;

		root = maxNode;//以最大点为根节点,继续比较
	}
}

void heapSort(int *data, int count) {
	int root;
	int tmp;

    //先使二叉树变为大根堆
	for (root = count / 2 - 1; root > 0; root--) {
		adjustHeap(data, count, root);
	}
    //每次取出大根堆中的根节点,然后使他“沉”入数组“末端”
	while (count) {
		adjustHeap(data, count, 0);//每次以根节点为起点,生成大根堆

		tmp = data[0];
		data[0] = data[count - 1];
		data[count - 1] = tmp;
		--count;
	}
}

2、算法分析

堆排序的时间复杂度分为初始化堆和排序重建堆。

初始化堆阶段:

初始化堆是从下向上,从左向右进行的。假设高度为k,则从倒数第二层右边的节点开始,开始执行比较交换;倒数第二层,则会选择其子节点进行比较,若进行了交换,则继续选择子节点的子节点开始比较。上层也是这样。

总的时间:s = 2 ^ (i - 1) * (k - i); i表示第几层,2 ^ (i - 1)表示该层上有多少个元素, (k - i)表示子树要下调的深度。

则s = 2 ^ (k -2) * 1 + 2 ^ (k -3) * 2 +…… + 2 ^ (k -i) *(i -1) + 2 ^(0) *(k - 1)(叶子层不需交换),则i从k-1到1。

s = 2 ^ k -k -1,由因为k为完全二叉树的深度,而log(n) = k。

得到: S = n - log(n) -1,所以时间复杂度为O(n)

排序重建堆

每次重建意味着有一个节点出堆,所以需要将堆的容量减一。adjustheap()函数的时间复杂度k=log(n),k为堆的层数。所以在每次重建时,随着堆的容量的减小,层数会下降,函数时间复杂度会变化。重建堆一共需要n-1次循环,每次循环的比较次数为log(i),则相加为:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以证明log(n!)和nlog(n)是同阶函数:

∵(n/2)^(n/2) ≤ n! ≤ n^n

∴n/4log(n) = n/2log(n^(1/2)) ≤ n/2log(n/2) ≤ log(n!)≤nlog(n)

所以时间复杂度为O(nlogn)

则总的时间复杂度为O(n+nlogn)=O(nlogn)。

在此借鉴堆排序的时间复杂度分析

五、交换排序

1、算法简介

交换排序又名冒泡排序,就是让相邻两个数据进行比较,若前面的数据大于后面的数据,则进行交换。就像大泡泡往水底沉一样。

在这里插入图片描述

可以很清楚的看到,每次从头开始,截至到n-i(n为长度,为第几次循环)

代码如下:

void swapSort(int *data, int count) {
	int i;
	int j;
	int tmp;
	int hasSwap = 1;//hasSwap判断某次循环是否有交换,若没有交换,则说明已经排好序,这是直接结束循环

	for (i = 0; hasSwap && i < count - 1; i++) {
		hasSwap = 0;
		for (j = 0; j < count - i - 1; j++) {
			if (data[j] > data[j+1]) {
				tmp = data[j];
				data[j] = data[j+1];
				data[j+1] = tmp;
				hasSwap = 1;
			}
		}
	}	
}

2、算法分析

明显的可以看出:时间复杂度为O(n*(n -1) / 2);

最差情况:完全倒序

最优情况:完全顺序。

六、快速排序

1、算法简介

快速排序实际上是以头为基准,运用头指针和尾指针比较基准,使比基准小的数摆放在基准的左边,比基准大的数排放在基准的后面,然后此时这组数据已经被分为两半,然后除基准外的数,作为一个新的数据,继续执行前面的过程。

做法:

1.用tmp记录这组数据的第一个元素。

2.使head和tail分别指向数据的第一个元素和最后一个元素。

3.由于tmp已经记录了这个数,则以head为下标的数据可以视作空。

4.用tail指向的空间数据与tmp进行比较,若tail所指向的空间的数据大于等于tmp,则tail–,继续第4步;

5.若tail所指向的空间的数据小于tmp,则交换head所指向空间的数据和tail所指向的空间数据,同时head++;

6.用head指向的空间数据与tmp进行比较,若tail所指向的空间的数据小于等于tmp,则head++,继续第6步;

7.若head所指向的空间的数据大于tmp,则交换head所指向空间的数据和tail所指向的空间数据,同时tail–,执行第4步;

4~7一直只想到head > tail时,结束。

在这里插入图片描述

这样将每次的数据分为两半,然后再进行比较,我们可以使用递归来解决这个问题。

void quickOnce(int *data, int count, int head, int tail) {
	int tmp = data[head];//设置基准
	int start = head;
	int end = tail;

	if (head >= tail) {
		return;
	}

	while (head < tail) {
		while (head < tail && data[tail] >= tmp) {
			--tail;
		}
		if (head < tail) {
			data[head] = data[tail];
			++head;
		}
		while (head < tail && data[head] <= tmp) {
			++head;
		}
		if (head < tail) {
			data[tail] = data[head];
			--tail;
		}
	}
	data[head] = tmp;

    //分别递归基准左边的右边的数
	quickOnce(data, count, start, head - 1);
	quickOnce(data, count, head + 1, end);
}

void qucikSort(int *data, int count) {
	quickOnce(data, count, 0, count - 1);
}

2、算法分析

时间复杂度为:O(nlogn)。

由于笔者水平有限,现直接给出别人的解答。

快速排序 及其时间复杂度和空间复杂度

七、总结

这几天对这几类算法进行了复习,当自己手动去画出这些代码的步骤时,理解起来更加深刻。俗话说的好:眼过千遍,不如手过一遍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值