【数据结构】排序-C语言版

目录

一、插入排序

1.直接插入排序

2、希尔排序

二、选择排序

1.直接选择排序

2.堆排序

三、交换排序

1.冒泡排序 

2.快速排序

(1)hoare版本

(2)挖坑法 

(3)前后指针法

(4)快速排序优化-三数取中法

(5)快速排序-小区间优化

(6)快速排序-非递归

四、归并排序

1.归并-递归

2.归并-非递归

3.外排序

外排序应用

五、计数排序

六、排序总结


各类排序算法基本思想是什么?如何实现?时间复杂度分别是多少?稳定吗?

常见的排序算法有如下7种:

一、插入排序

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

1.直接插入排序

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

055-InsertSort.h 

#pragma once
#include<stdio.h>
#include <stdlib.h>


//打印
void Print(int* a, int n);

//直接插入排序
void InsertSort(int* a, int n);

 055-InsertSort.c

#include "055-InsertSort.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

//直接插入排序
void InsertSort(int* a, int n)
{
	//多趟排序
	for (int i = 0; i < n - 1; i++)
	{
		//把temp插入到数组的[0,end]有序区间中
		int end = i;
		int temp = a[end + 1];
		while (end >= 0)
		{
			if (temp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}

		a[end + 1] = temp;

	}
	
}

055-TestInsertSort.c

#include "055-InsertSort.h"

void TestInsertSort()
{
	int arr[] = { 9,1,2,5,7,4,8,6,3,5 };
	InsertSort(arr, sizeof(arr) / sizeof(arr[0]));
	Print(arr, sizeof(arr)/sizeof(arr[0]));
}

int main()
{
	TestInsertSort();

	return 0;
}

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

(1) 元素集合越接近有序,直接插入排序算法的时间效率越高

(2) 时间复杂度:O(N^2)

(3) 空间复杂度:O(1),它是一种稳定的排序算法

(4) 稳定性:稳定

2、希尔排序

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

 

 055-ShellSort.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//希尔排序
void ShellSort(int* a, int n);

 055-ShellSort.c

#include "055-ShellSort.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

//希尔排序 - 先分组,对分组的数据,插入排序
//1.预排序-接近有序
//2.直接插入
void ShellSort(int* a, int n)
{
	//gap>1的时候,预排序
	//gap==1的时候,直接插入排序
	int gap = n;
	while (gap > 1)
	{
		
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			//把temp插入到数组的[0,end]有序区间中
			int end = i;
			int temp = a[end + gap];
			while (end >= 0)
			{
				if (temp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
				
			}
			a[end + gap] = temp;
			
		}
		//printf("gap:%d-> ", gap);
		//Print(a, n);
	}
}

 055-TestShellSort.c

#include " 055-ShellSort.h"

void TestShellSort()
{
	int arr[] = { 10,7,8,9,6,5,4,3,2,1,0,-1,-2,-3,-4,-5,-6,-7,-8,-9 };
	ShellSort(arr, sizeof(arr) / sizeof(arr[0]));
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestShellSort();
	
	return 0;
}

希尔排序特性总结:

(1)希尔排序是对直接插入排序的优化。

(2) 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。整体而言,达到了优化的效果。
(3) 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定,实验和基础上推断时间复杂度为O(N^1.3)。
        ① 最坏的情况是逆序时,gap很大时,while循环的时间复杂度为O(N)
        ② 当gap很小时,本来应该是O(N*N) ,但是经过预排序后,数组已经接近有序,所以这里还是O(N)
(4) 稳定性:不稳定

二、选择排序

选择排序基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

1.直接选择排序

基本思想:
(1)在元素集合array[i]—array[n-1]中选择关键码最大(小)的数据元素。
(2)若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
(3)在剩余的array[i]—array[n-2](array[i+1]—array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。

 055-SelectSort.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//直接选择排序
void SelectSort(int* arr, int n);

055-SelectSort.c

#include"055-SelectSort.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

void Swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

//直接选择排序
void SelectSort(int* a, int n)
{
	int left = 0, right = n - 1;
	while (left < right)
	{
		//选出最大的值和最小的值
		int maxIndex = left, minIndex = left;
		for (int i = left; i <= right; i++)
		{
			if (a[i] < a[minIndex])
			{
				minIndex = i;
			}
			if (a[i] > a[maxIndex])
			{
				maxIndex = i;
			}
		}
		Swap(&a[left], &a[minIndex]);

		//如果maxIndex和left位置重叠,那么maxIndex位置的书就被换走了,要修正一下max的位置
		if (maxIndex == left)
		{
			maxIndex = minIndex;
		}

		Swap(&a[right], &a[maxIndex]);
		++left;
		--right;
	}	
}

055-TestSelectSort.c

#include "055-SelectSort.h"

void TestSelectSort()
{
	int arr[] = { 10,9,8,7 6,5,4,3,2,1};
	SelectSort(arr, sizeof(arr) / sizeof(arr[0]));

	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestSelectSort();

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

2.堆排序

堆排序(Heapsort)是利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。堆的排序过程请查看文章 【数据结构】堆-C语言版 一文中堆的排序小节
055-HeapSort.h
#pragma once
#include<stdio.h>
#include<stdlib.h>

//打印
void Print(int* a, int n);

//堆排序
void HeapSort(int* arr, int n);

055-HeapSort.c

#include "055-HeapSort.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

void Swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

//向下调整
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;
		}
		//小孩子比父亲大,跳出while循环,结束这一次的向下调整,否则一直死循环并且什么都不做
		else
		{
			break;
		}
	}
}

//堆排序
void HeapSort(int* a, int n)
{
	//升序,建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

 055-TestHeapSort.c

#include "055-HeapSort.h"

void TestHeapSort()
{
	int arr[] = { 10,9,8,7 };
	HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestHeapSort();

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

三、交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

1.冒泡排序 

基本思想:依次比较相邻的两个数,将较小数放在前面,较大数放在后面,如此继续,直到比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成

 055-BubbleSort.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, 

//冒泡排序
void BubbleSort(int* a, int n);int n);

 055-BubbleSort.c

#include "055-BubbleSort.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

void Swap(int* p1, int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

//冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n - i - 1; j++)
		{
			if (a[i] > a[j])
			{
				Swap(&a[i], &a[j]);
			}
		}
	}
}

055-TestBubbleSort.c

#include "055-BubbleSort.h"

void TestBubbleSort()
{
	int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
	HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestBubbleSort();

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

2.快速排序

是一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

(1)hoare版本

单趟排序 :选出一个key,一般是最左边的,或者是最右边,key放到正确位置上去,目的是让key左边的比key小,key右边的比key大。

当让最左为key时,让 right先走,right找比key小的值,left找比key大的值,都找到后交换left和right位置的值直到相遇。

055-QuickSortHoare.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//快速排序
void QuickSort_hoare(int* a, int begin, int end);

055-QuickSortHoare.c

#include "055-QuickSortHoare.h"

//快速排序
void QuickSortHoare(int* a, int begin,int end)
{
	if (begin >= end)
	{
		return;
	}
	int left = begin,  right = end;
	int key = left;

	while (left < right)
	{
		//找小
		while (a[right] >= a[key] && left < right)
		{
			right--;
		}

		//找大
		while (a[left] <= a[key] && left < right)
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}

	int meeti = left;
	
	Swap(&a[meeti], &a[key]);
	
	QuickSort_hoare(a, begin, meeti - 1);
	QuickSort_hoare(a, meeti + 1, end);
}

 055-TestQuickSortHoare.c

#include "055-QuickSortHoare.h"

void TestQuickSortHoare()
{
	int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
	QuickSortHoare(arr, 0,sizeof(arr) / sizeof(arr[0])-1);
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestQuickSortHoare();

	return 0;
}

(2)挖坑法 

key为左边第一个元素,把第一个元素挖出来,空出一个坑,left指向第一个元素,right指向最后一个元素,从右向左找比key小的元素放坑里,空出来一个坑,再从左向右找比key大的元素放坑里,如果left和right相遇,就把key放坑里,递归调用。

 055-QuickSortHole.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//快速排序hole
void QuickSortHole(int* a, int begin, int end);

 055-QuickSortHole.c

#include "055-QuickSortHole.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

//快速排序hole
void QuickSortHole(int* a, int left, int right)
{
	int begin = left, end = right;
	int key = a[left];
	
	while (left < right)
	{
		//找小
		while (a[right] >= key && left < right)
		{
			right--;
		}

		//放到左边的坑位中,右边就形成新的坑
		a[left] = a[right];
		
		//找大
		while (a[left] <= key && left < right)
		{
			left++;
		}

		//放到右边的坑位中,左边就形成新的坑
		a[right] = a[left];
	}

	a[left] = key;
	
	if (left - 1 > begin)
	{
		QuickSortHole(a, begin, left - 1);
	}
	if (end > left + 1)
	{
		QuickSortHole(a, left + 1, end);
	}
	
}

 055-TestQuickSortHole.c

#include "055-QuickSortHole.h"

void TestQuickSortHole()
{
	int arr[] = { 6,1,2,7,9,3,4,5,10,8 };
	QuickSortHole(arr, 0, sizeof(arr) / sizeof(arr[0]) - 1);
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestQuickSortHole();
	
	return 0;
}

(3)前后指针法

指定第一个元素为key,prev指针指向第一个元素位置,cur指针指向第二个元素位置

(1)当cur位置的元素大于key时,cur指向下一个位置。

(2)当cur位置的元素小于key时,让prev指向下一个元素位置,再判断prev和cur的位置是否相等:

        ①如果相等就让cur指向下一个位置

        ②如果不相等就让cur和prev位置的元素进行交换。

 055-QuickSortPointer.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//前后指针法
void QuickSortPointer(int a, int begin, int end);

 055-QuickSortPointer.c

#include "055-QuickSortPointer.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

//前后指针法
int PartSort(int* a, int left, int right)
{
	int key = a[left];
	int prev = left,cur = left+1;
	
	while (cur <= right)
	{
		//(a[cur] < key)&&(++prev!=cur) ===> Swap(&a[prev],&a[cur])
		if (a[cur] < key && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}

		//(a[cur] > key)||((a[cur] < key) && (++prev==cur))  ===> cur++
		cur++;
	}
	//cur > end ===> Swap(&a[prev],&key)
	Swap(&a[prev], &a[left]);
	return prev;
}


void QuickSortPointer(int a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int keyi = PartSort(a, begin, end);

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

 055-TestQuickSortPointer.c

#include "055-QuickSortPointer.h"

void TestQuickSortPointer()
{
	int arr[] = { 28,16,32,12,60,2,5,72 };
	QuickSortPointer(arr, 0, sizeof(arr) / sizeof(arr[0]) - 1);
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestQuickSortPointer();

	return 0;
}

(4)快速排序优化-三数取中法

 快速排序的时间复杂度是O(N),快排最理想的效率就是每次选择的key都是是下标为中位数的元素,即每次进行完单趟排序后,key的左序列与右序列的长度都相同,这时的时间复杂度为O(NlogN)。

 效率最低的的情况就是数组有序,那么每次选取的key都是序列中最左或最右的元素。如果每次选择的key都是最大或最小的,会退化成选择排序,时间复杂度变成了O(N^2)。

 由此可看出key越接近中间位置,效率越高。为了避开每次选择最大或最小的key,避免对数组有序的情况下使用快排排序,使用三数取中法:key取a[left]、a[mid]、a[right]三者进行比较后的值中居中的值,这就避免了每次选取的key不会是待排序数组中最大或最小值。

按照前面的代码,选取的key一般都是数组最左端的元素,如果上面拿到的居中值的位置不在最左端怎么办?将居中值和最左端元素a[left]交换一下即可,这就能保证a[left]一定是居中值,前面的代码也不需要改变。

055-QuickSortThreeNumberMid.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//三数取中法
void QuickSortThreeNumberMid(int a, int begin, int end);

055-QuickSortThreeNumberMid.c

#include "055-QuickSortThreeNumberMid.h"

//选取a[left]、a[mid]、a[right]中的居中值
int GetMidIndex(int* a, int left, int right)
{
	int mid =left + (left + right)/2;
	
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}

}

//快排
int PartSort1(int* a, int left, int right)
{
    //找居中值
	int mid = GetMidIndex(a,left,right);
    //交换居中值和最左端元素
	Swap(&a[left], &a[mid]);

	int key = left;
	while (left < right)
	{
		//找小
		while (a[right] >= a[key] && left < right)
		{
			right--;
		}

		//找大
		while (a[left] <= a[key] && left < right)
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}

	Swap(&a[left], &a[key]);

	return left;

}

void QuickSortThreeNumberMid(int a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	int keyi = PartSort1(a, begin, end);

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

055-TestQuickSortThreeNumberMid.c

#include "055-QuickSortThreeNumberMid.h"

void TestQuickSortThreeNumberMid()
{
	int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
	QuickSortThreeNumberMid(arr, 0, sizeof(arr) / sizeof(arr[0]) - 1);
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestQuickSortThreeNumberMid();

	return 0;
}

(5)快速排序-小区间优化

递归时,递归次数以2倍的形式快速增长。需要思考:

(1)如果递归到后面子区间数据较多,可以继续选key进行单趟排序,来分割子区间进行分治递归。

(2)如果递归到后面子区间数据较少,再用分支递归继续处理不太划算。

为了减少递归调用的次数,在递归调用的前面几层使用快速排序,而在递归调用的后面几层根据子区间的数据量选择其他排序,如希尔排序(比快排时间复杂度低)

void QuickSortThreeNumberMid(int a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

    //当待排序数据量>20时使用快排
    if(end - begin + 1 > 20)
    {
        int keyi = PartSort(a, begin, end);

	    QuickSortThreeNumberMid(a, begin, keyi - 1);
	    QuickSortThreeNumberMid(a, keyi + 1, end);
    }
    else  //当待排序数据量<=20时使用希尔排序
    {
        ShellSort(a,end-begin+1);
    }

}

(6)快速排序-非递归

快速排序递归方式最大的问题是假如递归深度太深的话,程序本身没有问题,但是栈空间不够时,会导致栈溢出。可以改成非递归方式,需要用栈存储数据模拟递归过程。

基本思路:

(1)将待排序数组第一个元素下标L和最后一个元素下标入栈R

(2)栈不为空时,每次读取调L和R,用单趟排序获取key的下标,判断key的左侧序列和右侧序列是否还需要排序:
        ①如果需要,入栈对应序列的L和R

        ②如果不需要(只有一个元素或没有元素需要排序了),不需要入栈

(3)一直执行2,直到栈空

//非递归实现
void QuickSortNonR(int* a, int begin, int end)
{
	stack st;
	StackInit(&st);
	StackPush(&st,begin);//入栈数组最左端元素下标L
	StackPush(&st, end);//入栈数组最右端元素下标R

	while (!StackEmpty(&st))
	{
		int left, right;

        //获取数组最右端元素下标R
		right = StackTop(&st);
		StackPop(&st);

        //获取数组最左端元素下标L
		left = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort1(a, left, right);

		if (keyi - left > 1)
		{
			StackPush(&st, left);//入栈左侧序列的左下表L
			StackPush(&st, keyi - 1);//入栈右侧序列的右下表R
		}

		if (right - keyi > 1)
		{
			StackPush(&st, keyi + 1);//入栈左侧序列的左下表L
			StackPush(&st, right);//入栈右侧序列的右下表R
		}
	}

	StackDestroy(&st);

}

 快速排序特性总结:

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

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

四、归并排序

基本思想:

也是一种分治法,将待排序数组看成为n个长度为1的有序子数组,把这些子数组两两归并,使得到「n/2」个长度为2的有序子数组;然后再把这「n/2」个有序数组的子数组两两归并,如此反复,直到最后得到一个长度为n的有序数组为止,这种排序方法也叫做二路归并排序。

1.归并-递归

归并需要借助第三方数组,否则会造成覆盖,在"治"的时候需要取两个区间的小的元素插入到第三方数组,直到一个区间结束了,再把另一个区间剩下的元素尾插到最后

055-MergeSort.h 

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//归并排序
void MergeSort(int a, int n);

055-MergeSort.c

#include "055-MergeSort.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

void _MergeSort(int* a, int left, int right, int* temp)
{
	if (left >= right)
	{
		return;
	}

	int mid = (left + right) / 2;
	_MergeSort(a, left, mid, temp);
	_MergeSort(a, mid + 1, right, temp);

	//将两段有序子区间归并到temp,并拷贝回去
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;

	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
        //将两段有序子区间的较小值拷贝到temp
		if (a[begin1] < a[begin2])
		{
			temp[i++] = a[begin1++];
		}
		else
		{
			temp[i++] = a[begin2++];
		}
	}
    
    //如果其中一个有序子区间没有拷贝完,将剩下的元素直接拷贝到temp中
	while (begin1 <= end1)
	{
		temp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		temp[i++] = a[begin2++];
	}

	int j = 0;

    //将temp数组拷回到a中
	for (j = left; j <= right; j++)
	{
		a[j] = temp[j];
	}
}

void MergeSort(int* a, int n)
{
    //申请额外空间
	int* temp = (int*)malloc(sizeof(int) * n);

	if (temp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
    
    //归并排序
	_MergeSort(a, 0, n - 1,temp);
    
    //释放空间
    free(temp);
}

055-TestMergeSort.c

#include "055-MergeSort.h"

void TestMergeSort()
{
	int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
	MergeSort(arr, sizeof(arr) / sizeof(arr[0]));
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestMergeSort();

	return 0;
}

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

2.归并-非递归

归并的非递归的实现需要控制每次归并的元素个数,先是1 1归并,再2 2归并,4 4归并……,因

非递归可以控制每次的gap成倍增长来达到归并的目的,但gap成倍增长也可能会带来这样的问题

(1)第一个区间的元素个数=gap,第二个区间的元素个数<gap

(2)第一个区间的元素个数<=gap,第二个区间的元素不存在

055- MergeSortNonR.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//归并非递归
void MergeSortNonR(int* a, int n);

055- MergeSortNonR.c

#include "055- MergeSortNonR.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

void _MergeSortNonR(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
	int i = begin1;
	int j = begin1;
	//将两段子区间进行归并,归并结果放在tmp中
	
	while (begin1 <= end1 && begin2 <= end2)
	{
		//将较小元素拷贝到tmp中
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}	
		else
		{
			tmp[i++] = a[begin2++];
		}
			
	}
	//如果其中一个区间结束,那么另一个区间剩余的元素直接拷贝到tmp中
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
		
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
		
	//归并结束,把tmp拷贝到原数组
	for (; j <= end2; j++)
	{
		a[j] = tmp[j];
	}
		
}


//归并排序非递归
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);//申请一个临时数组空间
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	
	int gap = 1;//需合并的子区间中元素的个数
	while (gap < n)
	{
		int i = 0;
		for (i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			
			//第一个区间的元素个数<=gap,第二个区间的元素不存在,不需要合并
			if (begin2 >= n)
			{
				break;
			}
			
			//第一个区间的元素个数=gap,第二个区间的元素个数<gap,则第二个小区间的右边界即为数组的右边界
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			_MergeSortNonR(a, tmp, begin1, end1, begin2, end2);//合并两个有序序列

		}
		gap *= 2;//下一趟需合并的子序列中元素的个数成倍增长
	}
	free(tmp);//释放temp空间
}

 055- TestMergeSortNonR.c

#include "055- MergeSortNonR.h"

void TestMergeSortNonR()
{
	int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
	MergeSortNonR(arr,  sizeof(arr) / sizeof(arr[0]));
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestMergeSortNonR();

	return 0;
}

3.外排序

内排序:数据量相对少,可以放到内存中排序

外排序:数据量较大,内存中放不下,数据需要放到磁盘中,需要排序

外排序应用

有10亿个整数,放到文件中,需要排序假设可以给512MB的内存,如何进行排序?

(1)4GB的数据,分8次读取完,每次读1/8即512MB到内存中进行排序,排序完毕后写到一个小文件,再继续读下一个1/8,重复这个过程,直到这8个文件都有序。

 注意:1/8大小的数据在内存中排序时,是内排序,不能用归并排序,因为归并排序要额外申请O(N)的空间,可以选择快排等排序。

(2)8个文件已有序,还需要对这8个文件进行排序,从前两个文件中分别读取一个数据进行排序后写到一个文件中,再分别读取两个数据进行排序写到文件中,直到这两个文件的数据都已经比较完毕,对其他文件也同样进行排序,输出4个文件。再重复前述操作,直到所有文件都排序完毕,输出一个4GB的有序文件。

五、计数排序

原理:用另一个数组统计原数组中相同元素出现的次数,再根据统计次数反向填充到原数组中

  

 需要考虑到,如果a中最小的元素值不是从0开始呢?而是从10000,100000开始呢?Count数组长度岂不是很大?而且前面的元素值统计没有意义,这会造成空间的浪费,因此Count数组的长度取决于数组a中最大最小元素值的绝对差。

055-CountSort.h

#pragma once
#include<stdio.h>
#include <stdlib.h>

//打印
void Print(int* a, int n);

//计数排序
void CountSort(int* a, int n)

 055-CountSort.c

#include "055-CountSort.h"

//打印
void Print(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}

	printf("\n");
}

//计数排序
void CountSort(int* a, int n)
{
	//Count数组的长度取决于数组a的元素大小的绝对差
	//为了计算绝对差,需要先找出数组a的最大最小值
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}

	//绝对差作为申请分配新数组空间的个数
	int range = max - min + 1;
	int* Count = (int*)malloc(sizeof(int) * range);

	if (Count == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	//初始化Count数组
	memset(Count, 0, sizeof(int)*range);

	//Count数组赋值为数组a的元素出现的次数
	for (int i = 0; i < n; i++)
	{
		Count[a[i]-min]++;
	}

	//根据统计次数反向填充到原数组a中
	int i = 0;
	for (int j = 0; j < range; j++)
	{
		while (Count[j]--)
		{
			a[i++] = j + min;
		}
	}
}

055-TestCountSort.c

#include "055-CountSort.h"

void TestCountSort()
{
	int arr[] = { 2,5,3,0,2,3,0,3 };
	CountSort(arr, sizeof(arr) / sizeof(arr[0]));
	Print(arr, sizeof(arr) / sizeof(arr[0]));
}

int main()
{
	TestCountSort();

	return 0;
}

适用场景:待排序数组的范围比较集中,效率就高,否则Count数组长度很长,浪费空间;且只适合整数,浮点数和字符串不适用。

时间复杂度:O(n+range)

空间复杂度:O(n)

六、排序总结

 稳定的定义:数组中相同的值,排完序后,相对顺序不变,就是稳定的,否则就是不稳定的。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值