【数据结构】八大排序

目录

排序分类:

一、插入排序:

1.1 基本思想:

1.2 直接插入排序:

1.3直接插入排序的特性总结:

二、希尔排序(缩小增量排序):

2.1基本思想:

2.2 希尔排序代码实现:

2.3 希尔排序的特性总结:

三、选择排序:

3.1 基本思想:

3.2 直接选择排序:

3.3 直接选择排序的特性总结:

四、堆排序:

4.1 基本思想:

4.2 代码实现:

4.3直接选择排序的特性总结:

五、冒泡排序:

5.1 基本思想:

5.2 代码实现:

5.3 冒泡的特性总结:

六、快速排序:

6.1 基本思想:

6.2 快速排序-递归实现:

6.3 快速排序-非递归实现:

6.4 快速排序的特性总结:

七、归并排序:

7.1 基本思想:

7.2 归并排序-递归实现:

7.3 归并排序-非递归实现:

7.4 归并排序的特性总结:

八、计数排序:

8.1 基本思想:

8.2 计数排序实现: 

8.3  计数排序的特性总结:

九、排序特性总结:

​编辑


排序分类:

一、插入排序:

1.1 基本思想:

直接插入排序是一种简单的插入排序法,其基本思想是: 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为 止,得到一个新的有序序列

 实际中我们玩扑克牌时,就用了插入排序的思想

1.2 直接插入排序:

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

 

 代码实现:

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n-1; i++)
	{
		int end = i;
		int tmp = a[end + 1];

		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end+1] = tmp;
	}
}

1.3直接插入排序的特性总结:

1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度: O(N^2)
3. 空间复杂度: O(1) ,它是一种稳定的排序算法
4. 稳定性:稳定

什么是排序的稳定性?在对一组数据进行排序时,难免会有相同重复的值,在排序前后,如果相同的值的相对位置没发生改变,那么我们则称该排序稳定。以链表为例,就算是相同的值,储存它的节点的地址不同。

二、希尔排序(缩小增量排序):

2.1基本思想:

希尔排序法又称缩小增量法。希尔排序法的基本思想是: 先选定一个整数,把待排序文件中所有记录分成gap个 组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取重复上述分组和排序的工 作。当到达 =1 时,所有记录在统一组内排好序

 

 下面以9,1,2,5,7,4,8,6,3,5演示一次排序:

 假设gap = 3,那么分成的组为:

1. 9 5 8 5

2. 1 7 6

3. 2 4 3

分组后分别排序:

1. 5 5 8 9

2. 1 6 7

3. 2 3 4

经过一组gap排序后数据为:5 1 2 5 6 3 8 7 4 9

一次gap组依次排序被称为预排序,代码实现为:

void ShellSort(int* a, int n)
{
	//预排序
	int gap = 3;
	for (int j = 0; j < gap; j++)
	{
		for (int i = j; i < n - gap; i+=gap)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end-=gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

对比直接插入排序与预排序可以看出,预排序是在插入排序的基础上进行改变,直接插入排序中的gap = 1,而预排序中的第一次的gap = 3,接着gap逐渐减小直到为1。预排序将混乱的数据有序化,随着gap减小,数据愈发有序,当gap = 1,就是进行直接插入排序,这样的排序方法可以大大缩短排序时间,这就是希尔排序的思想。当然数据庞大时,gap不能从3开始,那么如何判断gap的取值呢?我们可以让gap = gap / 3+1为什么加个1呢?如果是gap/3那么gap可能会有等于0的情况,对于以上预排序的代码,如果我们再加入gap递减排序的功能,就需要进行四层循环,针对此情况我们可以对代码进行改进,让几组gap排序进行并排,即一起排序,即可减少一层循环。

2.2 希尔排序代码实现:

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap>1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

2.3 希尔排序的特性总结:

1. 希尔排序是对直接插入排序的优化
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:

4. 稳定性:不稳定 

三、选择排序:

3.1 基本思想:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
 
1.在元素集合 array[i]--array[n-1] 中选择关键码最大 ( ) 的数据元素
2.若它不是这组元素中的最后一个 ( 第一个 ) 元素,则将它与这组元素中的最后一个(第一个)元素交换
3.在剩余的 array[i]--array[n-2] array[i+1]--array[n-1] )集合中,重复上述步骤,直到集合剩余 1 个元素

3.2 直接选择排序:

代码实现:

void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		int min = begin, max = begin;
		for (int i = begin+1; i <= end; i++)
		{
			if (a[i] < a[min])
			{
				min = i;
			}
			if (a[i] > a[max])
			{
				max = i;
			}
		}
		Swap(&a[begin], &a[min]);
		//begin位置的值被换到min位置,修改max
		if (max == begin)
		{
			max = min;
		}
		Swap(&a[end], &a[max]);
		begin++;
		end--;
	}
}

3.3 直接选择排序的特性总结:

1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度: O(N^2)
3. 空间复杂度: O(1)
4. 稳定性:不稳定

四、堆排序:

4.1 基本思想:

堆排序 (Heapsort) 是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

4.2 代码实现:

void AdjustDown(HPDatatype* a,int parent,int n)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;
		}
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	//将数组调整建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, n);
	}
	//排序
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, 0, end);
		end--;
	}

}

4.3直接选择排序的特性总结:

1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度: O(N*logN)
3. 空间复杂度: O(1)
4. 稳定性:不稳定

五、冒泡排序:

5.1 基本思想:

冒泡排序相信大家都非常熟悉,在次就不多赘述,接下来我们将其优化一下,在排序的过程中,我们设置一个flag,来判断是否相邻数据之间发生交换,如果一次排序走下来,没有数据发生交换,那么则说明数据是有序的,退出程序即可。

 

5.2 代码实现:

void BubbleSort(int* a, int n)
{
	for (int i = 0;i < n; i++)
	{
		int flag = 0;
		for (int j = 1; j < n - i; j++)
		{
			if (a[j - 1] > a[j])
			{
				Swap(&a[j - 1], &a[j]);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

5.3 冒泡的特性总结:

1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度: O(N^2)
3. 空间复杂度: O(1)
4. 稳定性:稳定

六、快速排序:

6.1 基本思想:

快速排序是 Hoare 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
 if(right - left <= 1)
 return;
 
 // 按照基准值对array数组的 [left, right)区间中的元素进行划分
 int div = partion(array, left, right);
 
 // 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
 // 递归排[left, div)
 QuickSort(array, left, div);
 
 // 递归排[div+1, right)
 QuickSort(array, div+1, right);
}

其中,partion函数作用为,选出keyi的位置,并使大于a[keyi]的数据在其右边,小于其的数据在左边,keyi一般在开头或者结尾的位置。如下图:

 确定keyi排序后的正确位置后,我可以得到两段区间。即【begin , keyi-1】 ,【keyi+1,end】,然后将这两段区间递归下去,排序子区间,直到整个数组有序。

6.2 快速排序-递归实现:

将区间按照基准值划分为左右两半部分的常见方式:

1.hoare版本:

 单趟排序步骤:

1.选出keyi,一般选择头或者尾数据为keyi。

2.设置left,right,left从左向右走找比a[keyi]大的数,right从右向左走找比a[keyi]小的数,但两个位置并不是同时走的,keyi在左,right先走(right停下时的值一定比a[keyi]小),keyi在右,left先走,这样可以保证数据小于a[keyi]的都在左,大于的在右边,如果keyi在右边,那就让left先走,效果是相同的。

3.我们以keyi在左为例,right从右边先走,找比a[keyi]小的数,找到就停,然后left从左开始找比a[keyi]大的数,找到停下,交换两个位置的数据,重复此步骤,直到left与right相遇,将a[keyi]与相遇位置的值交换,这就是一趟排序的整个流程。

图示:

 

 代码实现:

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int left = begin, right = end;
	int keyi = left;
	while (left < right)
	{
		//小心越界!!相等的也直接越过
		while(left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while(left<right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[right]);
	keyi = right;

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

看一下递归展开图:

思考一下如果数据是有序的上图会是什么形式?

如上图, 当数据有序时,将keyi设置在左边时,会大大增加递归的深度,从而增大程序的复杂度,那么如何解决这个问题优化整个程序呢?那么就让keyi取一个随机的位置,但随机的值也并非好控制,经过思考,keyi尽量取中间值,那么如何让keyi取中间值?

优化-三数取中:

将整个数据中的 left mid right 比较,取三个位置中的中间大小的数据的下标为keyi,再将a[keyi]与a[left]交换一下,这样可以保证取得keyi不是整个数据中最大或者最小的值。

 代码实现:

int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
}

小区间优化:

观察上边的递归展开图,发现递归的过程似乎和满二叉树十分相似,由二叉树的性质可知,越向下二叉树的节点越多,最后一层的节点甚至占整个二叉树的一半,那么同理,递归越深,递归的次数越多,最后一层递归所开的空间相当于整体的50%,那么为节省递归所开辟的栈的空间,我们可不可以将最后三层用其他的排序来代替,这样既节省空间,又能提高整个程序的性能。

代码实现:

	if ((end - begin + 1) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}

优化后整体代码实现: 

int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if ((end - begin + 1) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int mid = GetMidIndex(a, begin, end);
		Swap(&a[mid], &a[begin]);

		int left = begin, right = end;
		int keyi = left;
		while (left < right)
		{
			//小心越界!!相等的也直接越过
			while(left < right && a[right] >= a[keyi])
			{
				right--;
			}
			while(left < right && a[left] <= a[keyi])
			{
				left++;
			}
			Swap(&a[left], &a[right]);
		}
		Swap(&a[keyi], &a[right]);
		keyi = right;

		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

 2.挖坑法:

 单趟排序步骤:

1.选出key,并让key保留a[key]的值。

2.设置一个“洞”,让hole在key原先的位置,即hole = left,同样,left从左走找大值,right从右走找小值,也是分别查找,但与hoare不同的是,当right找到小值,将a[right]的值赋给a[hole],而right的位置成为新的“洞”,即hole = right。

3.当left与right相遇时,将key存储的值赋给a[hole]。

 图示:

代码实现(已优化):

int PartSort2(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);

	int left = begin, right = end;
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		while(left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		while(left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if ((end - begin + 1) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort2(a, begin, end);

		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

 3.前后指针版本:

 单趟排序步骤:

1.同样三数取中选出key

2.设置两个指针,prev,cur,令prev = left,cur = left+1,两个指针都是从左向右走,cur指针找比a[key]小的值,cur找到小后,prev前进一步,即prev++,然后交换a[prev]和a[cur],cur指针接着找小,重复此步骤,直到遍历完整组数据,即cur > end,而prev指针所在的位置就是key的排序后位置,交换a[key],a[prev]。

3.cur是一直前进的,prev只在cur找到小值后前进一步。

 图示:

代码实现:

int PartSort3(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);
	int key = begin;
	int prev = begin, cur = begin + 1;
	while (cur <= end)
	{
		while (a[cur] < a[key] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[key], &a[prev]);
	key = prev;
	return key;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if ((end - begin + 1) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a, begin, end);

		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

快速排序完整代码实现:

int GetMidIndex(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[begin] < a[mid])
	{
		if (a[mid] < a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return begin;
		}
		else
		{
			return end;
		}
	}
	else
	{
		if (a[mid] > a[end])
		{
			return mid;
		}
		else if (a[begin] > a[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
}

//hoare
int PartSort1(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[mid], &a[begin]);

	int left = begin, right = end;
	int keyi = left;
	while (left < right)
	{
		//小心越界!!相等的也直接越过
		while(left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while(left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[right]);
	keyi = right;
	return keyi;
}

//挖坑法
int PartSort2(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);

	int left = begin, right = end;
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		while(left < right && a[right] >= key)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;
		while(left < right && a[left] <= key)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
	return hole;
}

//前后指针
int PartSort3(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	Swap(&a[begin], &a[mid]);
	int key = begin;
	int prev = begin, cur = begin + 1;
	while (cur <= end)
	{
		while (a[cur] < a[key] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[key], &a[prev]);
	key = prev;
	return key;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	if ((end - begin + 1) < 15)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a, begin, end);

		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

6.3 快速排序-非递归实现:

程序一旦涉及到递归,就要考虑递归深度的问题,快排亦是如此,在某种极端的情况下,快排很有可能,递归过深导致栈溢出,为解决这种情况,我们采用栈来模拟实现递归过程,即快排的非递归实现。

非递归实现步骤:

用栈来实现快速排序,本质上还是排序,栈仅仅是用来存储数据的下标,来选择排序哪个区间,假设10个数进行快速排序,首先我们需要对区间0到9进行排序,选出其中的key,再对[left,key-1],[key+1,right] 进行排序,选key的步骤运用上边三种方式的其中一种就可以,排序0到9区间,就将下标0,9入栈,根据栈的先进后出的性质,依次取出这段区间,即right = 9,left = 0,将其进行排序并找到key的位置,这样子区间就确定了,然后将两端子区间下标入栈,即left,key-1,key+1,right,(按这个顺序入栈就是先排序右子区间)重复以上步骤,直到栈为空,当然在将子区间入栈时,需要判断区间是否存在,即是否left < key-1,key+1 < right。

 图示:

代码实现:

void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		int key = PartSort3(a, left, right);

		if (key + 1 < right)
		{
			StackPush(&st, key + 1);
			StackPush(&st, right);
		}
		if (left < key - 1)
		{
			StackPush(&st, left);
			StackPush(&st, key - 1);
		}
	}
	StackDestroy(&st);
}

6.4 快速排序的特性总结:

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫 快速 排序
2. 时间复杂度: O(N*logN)

 

3. 空间复杂度: O(logN)
4. 稳定性:不稳定

七、归并排序:

7.1 基本思想:

归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法( Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

在这里插入图片描述

7.2 归并排序-递归实现:

 归并排序步骤:

归并排序的大体思路就是将整个数组分解,将分解的两个数组分别有序化,这样问题就转化为将两个有序化的数组合并成一个有序数组,即是依次对比两个数组进行尾插,那么如何将两个分解的数组有序化?将分解得到的两个数组看成左右子树,显然是用递归的办法接着将问题拆分,将数组拆分成更小的数据,直到分解成只剩一个数,一个数组中就只有一个数,那么它显然是有序的,就可以直接return递归回去。有递必然有归,单个数的数组递归回来,则变成两个只有一个数的数组,对这两个数组进行有序化,即归并这两个数组使其有序,两个数组有序化后,再递归回上一函数栈帧,再和另一个数组进行归并,依次类推就可以实现上边提到的两个数组归并使整个数组有序化。

 递归展开图:

合并两个有序数组,显然我们需要再创建一个数组用来尾插数据,那为什么不在原数组进行尾插或者采用交换的方式?对原数组直接进行操作,可能会导致有些数据被覆盖或打乱有序数组,所以我们需要malloc个一个新数组用来尾插归并数组,然后将排序好的数据复制回原数组(memcpy)。

代码实现: 

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	memcpy(a+begin, tmp+begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;
}

函数栈帧展开图:

7.3 归并排序-非递归实现:

与快排类似,递归的程序往往会有递归过深,栈溢出的情况,归并排序也有非递归的实现方式,与快排不同的是,归并的非递归不需要借栈或队列等数据结构,快排的非递归可以看成模拟递归,而归并的非递归则是以循环的方式进行的。

 非递归实现步骤:

首先,我们知道当数组中只有一个数时,我们认定这个数组是有序的,非递归就是在这个理论上反着这实现归并整个数组,假设一个rangeN,先让rangeN=1,rangeN是代表一层循环中,参与归并的数组中的数据个数,rangN=1时其实就是两个两个数的归并数据,当归并完整个数组,让rangeN*2,就是四个四个数一起归并,直到整个数组有序。

图示:

代码实现: 

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	int rangeN = 1;
	while (rangeN < n)
	{
		for (int i = 0; i < n; i += rangeN * 2)
		{
			int begin1 = i, end1 = i + rangeN - 1;
			int begin2 = i + rangeN, end2 = i + rangeN*2 - 1;
			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			//归并一部分拷贝一部分
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		rangeN *= 2;
	    //整体排序完后拷贝
	    //memcpy(a, tmp, sizeof(int) * n);
	}


	free(tmp);
	tmp = NULL;
}

但是,归并的过程中没有限制的话不会发生越界吗?答案是肯定的。越界的情况有以下三种:

1.end1,begin2,end2越界

2.begin2,end2越界

3.end2越界

如何解决越界情况呢?有两种解决办法:

1.修正越界的区间,就赋值给越界的区间一个不存在的区间,这种办法不会影响拷贝,整体拷贝或者部分拷贝都可以。

2.直接break跳出循环,进行下一组rangeN的归并,但这种方法不可以整体拷贝,因为break后,越界的数据并没有归并,也就是没有放入tmp数组,整体拷贝回原数组会导致原数组中的数组被覆盖,丢失。

代码实现:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	int rangeN = 1;
	while (rangeN < n)
	{
		for (int i = 0; i < n; i += rangeN * 2)
		{
			int begin1 = i, end1 = i + rangeN - 1;
			int begin2 = i + rangeN, end2 = i + rangeN*2 - 1;
			int j = i;
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
				//break;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
				//break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			//归并一部分拷贝一部分
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		rangeN *= 2;
		//整体排序完后拷贝
        //memcpy(a, tmp, sizeof(int) * n);
	}
	free(tmp);
	tmp = NULL;
}

7.4 归并排序的特性总结:

1. 归并的缺点在于需要 O(N) 的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度: O(N*logN)
3. 空间复杂度: O(N)
4. 稳定性:稳定

八、计数排序:

8.1 基本思想:

对于计数排序,其用到了哈希的思想,因此了解这个排序的前提需要理解哈希的思想。

**思想:**计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:

1. 统计相同元素出现次数
2.根据统计的结果将序列回收到原来的序列中

 

计数排序步骤:

定义一个count数组,让count数组的下标与原数组中的值正常大小顺序对应,遍历一遍原数组,统计每个数出现的次数,然后再根据count数组中下标的顺序以及出现的次数,打印回原数组,出现几次就打印几次。

让原数组与count数组中的数顺序对应,我们成为映射,映射分为两种:

1.绝对映射,假如原数组中是{5,8,3,7,9,1},那么count数组下标就是0,1,2,3,4,5,6,7,8,9count数组就是9个数那么大的空间。即count[a[i]]

2.相对映射:让原数组中的每个值减去整个数组中的最小值,来创建count数组,比如原数组{1000,1005,1003,1004,1006},那么count数组的下标就为0,1,2,3,4,5,6,大小就为7个数那么大的空间。即count[a[i]-min]

而count数组的大小需要求出原数组中的最大最小值来确定。

8.2 计数排序实现: 

代码实现:

void CountSort(int* a, int n)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (max < a[i])
		{
			max = a[i];
		}
		if (min > a[i])
		{
			min = a[i];
		}
	}

	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail");
		exit(-1);
	}

	//统计个数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}
	//排序
	int k = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[k++] = i + min;
		}
	}
	free(count);
	count = NULL;
}

可以看出,这种计数方法有弊端,它只能对整数进行排序,无法解决浮点数的问题。 

8.3  计数排序的特性总结:

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度: O(MAX(N, 范围 ))
3. 空间复杂度: O( 范围 )

九、排序特性总结:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值