八大排序算法

文章目录

  • 常见的排序算法
    • 直接插入排序
    • 希尔排序
    • 选择排序
    • 堆排序
    • 冒泡排序
    • 快速排序
      • 1.hoare版本
      • 2.挖坑版本
      • 前后指针版本
      • 分割左右区间进行递归
      • 三数取中
      • 小区间优化
      • 整体代码展示
      • 性能测试
      • 快速排序的非递归
      • 为什么要先从最右边出发?
    • 归并排序
      • 递归实现
      • 非递归实现
    • 非比较排序——计数排序


常见的排序算法

八大排序算法_第1张图片

直接插入排序

基本思想:
1.默认第一个元素有序;
2.依次取待排序的元素插入到已有序的部分中
3.通过不断向后挪动有序部分的元素,来找到第一个大于待排序元素的元素,把待排序元素插入到它的后面;
4.重复上述过程,直到完全有序;

八大排序算法_第2张图片

代码实现:
每次从第一个待排序的元素入手,让它分别与前面的有序数组的尾元素进行比较,如果大于尾元素,那么此时就已经是有序了;如果此时小于尾元素,那么尾元素就要向后挪动一步,并且尾元素指向原尾元素的上一个元素;重复上述过程,直到找到一个比待排序元素小的元素,并把待排序元素插入到其后面;

//直接插入排序
void InsertSort(int* nums, int numsSize)
{
	for (int i = 0; i < numsSize - 1; i++)
	{
		int endi = i;//有序数组的尾元素
		int tmp = nums[endi + 1];//记录下待排序元素

		//寻找第一个小于待排序元素的元素
		while (endi >= 0)
		{
			//如果待排序元素小于尾元素
			if (tmp < nums[endi])
			{
				//尾元素向后挪动
				nums[endi + 1] = nums[endi];
				//刷新尾元素
				endi--;
			}
			//如果排序元素大于尾元素,找到要插入的位置了
			else
			{
				break;
			}
		}
		//插入到尾元素的下一个位置
		nums[endi + 1] = tmp;
	}
}

直接插入排序特性总结:
1.元素集合越接近有序,直接插入排序时间效率越高;
2.时间复杂度O(N^2);
3.空间复杂度O(1);
4.稳定性:稳定;(可以控制相等的数的相对顺序);

希尔排序

希尔排序是直接插入排序的改进;希尔排序又称缩小增量法;希尔排序是将待排序的数组元素 按下标的一定增量分组 ,分成多个子序列,然后对各个子序列进行直接插入排序算法排序(使得每个子序列呈现有序,使大的数更快的到达数组后面,小的数更快的到达数组前面);然后依次缩减增量再进行排序,直到增量为1时,进行最后一次直接插入排序,排序结束。

八大排序算法_第3张图片
代码实现:

//希尔排序
void ShellSort(int* nums, int numsSize)
{
	int gap = numsSize;
	while (gap > 1)
	{
		gap = gap / 2;
		for (int i = 0; i < numsSize - gap; i++)
		{
			//插入排序
			int endi = i;
			int tmp = nums[endi + gap];
			while (endi >= 0)
			{
				if (nums[endi] > tmp)
				{
					nums[endi + gap] = nums[endi];
					endi -= gap;
				}
				else
					break;
			}
			
			nums[endi + gap] = tmp;
		}
	}
}

希尔排序的特性总结:
1.希尔排序是对直接插入排序的优化。
2.当gap > 1时都是预排序,目的是让数组更接近有序。当gap == 1时,数组已经接近有序的了,这样就整体而言,可以达到优化的效果。
3.希尔排序的时间复杂度不好计算,我们这里只需要记住它的时间复杂度是O(N^1.3 —— N ^2);
4.稳定性:不稳定;

选择排序

基本思想:
每一次从待排序的数据元素中选出最大和最小的一个元素,将他们分别存放在起始位置和尾部分,直到全部待排序的数据元素排完。

操作步骤:
1.在元素集合nums[0] – nums[n-1]中选择最大和最小的数据元素;
2.若它不是这数组元素中的最后一个(或第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换;
3.在剩余nums[1] – nums[n-2]集合中,重复上述步骤,直到集合剩余1个元素;
八大排序算法_第4张图片

代码实现:

//选择排序
void SelectSort(int* nums, int numsSize)
{
	int begin = 0, end = numsSize - 1;
	while (begin < end)
	{
		int maxi = begin, mini = begin;
		for (int i = begin; i <= end; i++)
		{
			//找出最大值的下标
			if (nums[i] > nums[maxi])
				maxi = i;

			//找出最小值的下标
			if (nums[i] < nums[mini])
				mini = i;
		}
		
		swap(&nums[begin], &nums[mini]);
		//当最大值在起始位置时,需要特殊判断
		//因为在交换最小值时会将最小值与最大值的位置更换掉
		if (maxi == begin)
		{
			maxi = mini;
		}
		swap(&nums[end], &nums[maxi]);
		begin++;
		end--;
	}
}

选择排序特性总结:
1.直接选择排序非常好理解,但是效率确是最差的一种。实际中很少运用;
2.时间复杂度: O(N^2);
3.空间复杂度: O(1);
4.稳定性:不稳定;

堆排序

堆排序是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种,它是通过堆来进行选择数据。

代码实现:

//向下调整算法
void AdjustDown(int* nums, int parent, int size)
{
	int child = parent * 2 + 1;
	
	while (child < size)
	{
		if (child + 1 < size && nums[child + 1] > nums[child])
			child++;

		if (nums[child] > nums[parent])
		{
			swap(&nums[child], &nums[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}

//堆排序
void HeapSort(int* nums, int numsSize)
{
	//向下调整建堆
	for (int i = (numsSize - 2) / 2; i >= 0; i--)
	{
		AdjustDown(nums, i, numsSize);
	}

	//选数,向下调整
	for (int i = 1; i < numsSize; i++)
	{
		swap(&nums[0], &nums[numsSize - i]);
		AdjustDown(nums, 0, numsSize - i);
	}
}

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

冒泡排序

基本思想 冒泡排序是一种交换排序,核心是冒泡,把数组中最大的那个往后冒,冒的过程就是和他相邻的元素交换。 重复走访要排序的数列,通过两两比较相邻记录的排序码。 排序过程中每次从前往后冒一个最大值,且每次能确定一个数在序列中的最终位置
八大排序算法_第5张图片

代码实现:

//冒泡排序
void BubbleSort(int* nums, int numsSize)
{
	//进行的趟数
	for (int i = 0; i < numsSize - 1; i++)
	{
		int flag = 1;
		//每趟冒泡进行的次数
		for (int j = 0; j < numsSize - 1 - i; j++)
		{
			if (nums[j] > nums[j + 1])
			{
				flag = 0;
				swap(&nums[j + 1], &nums[j]);
			}
		}
		//如果在冒泡过程中没有进行交换,说明数组已经有序,不需要在继续
		if (flag == 1)
			break;
	}
}

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

快速排序

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

将区间按照基准值划分为左右两半部分的常见方法有:
1.hoare版本
2.挖坑版本
3.前后指针版本
我们依次对三种版本进行讲解:

1.hoare版本

从最右边出发,找出小于基准值的值;再从最左边出发,找出大于基准值的值;然后交换它们的值;重复上述操作,直到左右相遇就停止;这样就完成了划分——一个值到达了它该到的位置,且左边是小于它的区间,右边是大于它的区间;
八大排序算法_第6张图片
代码实现:

//hoare版本
int Part1Sort(int* nums, int left, int right)
{
	int keyi = left;

	while (left < right)
	{
		//从右边出发寻找小于key的值
		while (left < right && nums[right] >= nums[keyi])
		{
			right--;
		}

		//从左边出发寻找大于key的值
		while (left < right && nums[left] <= nums[keyi])
		{
			left++;
		}

		//交换两个值
		if(left < right)
			swap(&nums[left], &nums[right]);
	}
	//让keyi与交点处交换
	swap(&nums[left], &nums[keyi]);

	return left;
}

2.挖坑版本

先选定基准值,并把最左边的值记录下,并作为坑位;从最右边开始,寻找小于基准值的值,并把它填到坑位中去,让这个下标作为新的坑位;再从左边开始,寻找大于基准值的值,并把它填入坑位中,让这个下标重新作为新的坑位;重复上述过程,直到左右相遇;最后再把基准值填入坑位中;同样的,这种方法能够让一个数到达它该到达的位置,并且其左区间皆小于它,右区间皆大于它
八大排序算法_第7张图片

代码实现:

//挖坑版本
int Part2Sort(int* nums, int left, int right)
{
	int key = nums[left];
	int hole = 0;//起始为最左边
	
	while (left < right)
	{
		//从最右边出发,找小于key的值
		while (left < right && nums[right] >= key)
		{
			right--;
		}
		//先填坑,再挖坑
		nums[hole] = nums[right];
		hole = right;

		//从最左边出发,找大于key的值
		while (left < right && nums[left] <= key)
		{
			left++;
		}
		nums[hole] = nums[left];
		hole = left;
	}
	
	//将key填入坑中
	nums[hole] = key;
}

前后指针版本

设置两个指针,让一个指针找出比key小的值,然后另一个指针前进一步并进行交换。八大排序算法_第8张图片
注意:
每次prev都会停留在比基准值小的地方,往后走一步就是比基准值大或等于的地方。

//前后指针版本
int Part3Sort(int* nums, int left, int right)
{
	int keyi = left;
	int cur = left + 1;
	int prev = left;
	
	while (cur <= right)
	{
		//如果遇到比基准值小的,就让prev前进一步,并判断是否可以交换
		if (nums[cur] < nums[keyi] && ++prev != cur)
		{
			swap(&nums[cur], &nums[prev]);
		}
		cur++;
	}
	swap(&nums[keyi], &nums[prev]);

	return prev;
}

分割左右区间进行递归

通过上述三种方法,我们都能得到一个值,来划分左右区间(左区间皆小于这个值,右区间皆大于这个值);我们不断递归这个分割过程,直到区间无法继续分割后返回;这样每个数都能通过递归到达它该到达的地方;

代码实现:

//快速排序
void QuickSort(int* nums, int left, int right)
{
	if (left >= right)
		return;

	int mid = Part3Sort(nums, left, right);
	
	//左区间递归
	QuickSort(nums, left, mid - 1);
	//右区间递归
	QuickSort(nums, mid + 1, right);
}

三数取中

在某种情况下,例如数组是已经是正序时,递归的呈现会出现大头现象,如下

八大排序算法_第9张图片
我们先对5w个数进行希尔排序,排完后就是有序的了,再对它进行快排,来看看效率如何:

int N = 50000;
int* a1 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; i++)
{
	a1[i] = rand();
}
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();

int begin6 = clock();
QuickSort(a2, 0, N - 1);
int end6 = clock();

printf("ShellSort:%d\n", end2 - begin2);
printf("QuickSort:%d\n", end6 - begin6);

在这里插入图片描述
可以看到,当数组有序时,快排是非常慢的;这种时候就无法选定最左边的值为基准值;这时候就有人提出了三数取中的办法,三数取中即:选出最左边的值,正中间的值,最右边的值三个书中大小为中的那个数;

代码实现:

//三数取中
int GetMidIndex(int* nums, int left, int right)
{
	int mid = (left + right) / 2;
	
	if (nums[left] > nums[right])
	{
		if (nums[right] > nums[mid])
			return right;

		else if (nums[mid] > nums[left])
			return left;

		else
			return mid;
	}
	else//nums[right] > nums[left]
	{
		if (nums[left] > nums[mid])
			return left;
		else if (nums[mid] > nums[right])
			return right;
		else
			return mid;
	}
}

以hoare版本的三数取中优化:

//hoare版本
int Part1Sort(int* nums, int left, int right)
{
	int mid = GetMidIndex(nums, left, right);
	swap(&nums[left], &nums[mid]);
	int keyi = left;

	while (left < right)
	{
		//从右边出发寻找小于key的值
		while (left < right && nums[right] >= nums[keyi])
		{
			right--;
		}

		//从左边出发寻找大于key的值
		while (left < right && nums[left] <= nums[keyi])
		{
			left++;
		}

		//交换两个值
		if(left < right)
			swap(&nums[left], &nums[right]);
	}
	//让keyi与交点处交换
	swap(&nums[left], &nums[keyi]);

	return left;
}

小区间优化

在递归过程中,递归的深度越深,其递归次数就成倍增加;为了解决递归深度太深而导致的递归次数过多,有人便提出了小区间优化这个概念:当区间个数小于某个值时,便不再递归下去,而是采用另外一种排序来替代递归;

这里我们采用直接插入排序来演示:

//快速排序
void QuickSort(int* nums, int left, int right)
{
	if (left >= right)
		return;

	//小区间优化
	if (right - left + 1 <= 8)
	{
		InsertSort(nums, right - left + 1);
		return;
	}

	int mid = Part3Sort(nums, left, right);
	
	//左区间递归
	QuickSort(nums, left, mid - 1);
	//右区间递归
	QuickSort(nums, mid + 1, right);
}

整体代码展示

//三数取中
int GetMidIndex(int* nums, int left, int right)
{
	int mid = (left + right) / 2;
	
	if (nums[left] > nums[right])
	{
		if (nums[right] > nums[mid])
			return right;

		else if (nums[mid] > nums[left])
			return left;

		else
			return mid;
	}
	else//nums[right] > nums[left]
	{
		if (nums[left] > nums[mid])
			return left;
		else if (nums[mid] > nums[right])
			return right;
		else
			return mid;
	}
}

//hoare版本
int Part1Sort(int* nums, int left, int right)
{
	int mid = GetMidIndex(nums, left, right);
	swap(&nums[left], &nums[mid]);
	int keyi = left;

	while (left < right)
	{
		//从右边出发寻找小于key的值
		while (left < right && nums[right] >= nums[keyi])
		{
			right--;
		}

		//从左边出发寻找大于key的值
		while (left < right && nums[left] <= nums[keyi])
		{
			left++;
		}

		//交换两个值
		if(left < right)
			swap(&nums[left], &nums[right]);
	}
	//让keyi与交点处交换
	swap(&nums[left], &nums[keyi]);

	return left;
}

//挖坑版本
int Part2Sort(int* nums, int left, int right)
{
	int mid = GetMidIndex(nums, left, right);
	swap(&nums[left], &nums[mid]);
	int key = nums[left];
	int hole = 0;//起始为最左边
	
	while (left < right)
	{
		//从最右边出发,找小于key的值
		while (left < right && nums[right] >= key)
		{
			right--;
		}
		//先填坑,再挖坑
		nums[hole] = nums[right];
		hole = right;

		//从最左边出发,找大于key的值
		while (left < right && nums[left] <= key)
		{
			left++;
		}
		nums[hole] = nums[left];
		hole = left;
	}
	
	//将key填入坑中
	nums[hole] = key;
}


//前后指针版本
int Part3Sort(int* nums, int left, int right)
{
	int mid = GetMidIndex(nums, left, right);
	swap(&nums[mid], &nums[left]);
	int keyi = left;
	int cur = left + 1;
	int prev = left;
	
	while (cur <= right)
	{
		//如果遇到比基准值小的,就让prev前进一步,并判断是否可以交换
		if (nums[cur] < nums[keyi] && ++prev != cur)
		{
			swap(&nums[cur], &nums[prev]);
		}
		cur++;
	}
	swap(&nums[keyi], &nums[prev]);

	return prev;
}


//快速排序
void QuickSort(int* nums, int left, int right)
{
	if (left >= right)
		return;

	//小区间优化
	if (right - left + 1 <= 8)
	{
		InsertSort(nums, right - left + 1);
		return;
	}

	int mid = Part3Sort(nums, left, right);
	
	//左区间递归
	QuickSort(nums, left, mid - 1);
	//右区间递归
	QuickSort(nums, mid + 1, right);
}

性能测试

我们用下面这个函数来对性能进行测试,由于选择,冒泡和直接插入性能都较差,所以我们先屏蔽掉它们,然后使用Release版本来进行测试;

void TestOP()
{
	int N = 5000000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);

	for (int i = 0; i < N; i++)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
	}

	int begin1 = clock();
	//InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	//SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	//BubbleSort(a5, N);
	int end5 = clock();

	int begin6 = clock();
	QuickSort(a6, N);
	int end6 = clock();

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("BubbleSort:%d\n", end5 - begin5);
	printf("QuickSort:%d\n", end6 - begin6);
}

八大排序算法_第10张图片

快速排序的非递归

快排的非递归其实就是模拟递归的过程,我们采用来存储每次分割出来的两个区间,然后根据这两个区间继续分割更多的子区间,直到无法继续分割为止。
下面我先给出栈的模板:


typedef int STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}Stack;

//栈的初始化
void StackInit(Stack* pst);
//栈的销毁
void StackDestory(Stack* pst);
//压栈
void StackPush(Stack* pst, STDataType val);
//出栈
void StackPop(Stack* pst);
//判空
bool StackEmpty(Stack* pst);
//取栈顶元素
STDataType StackTop(Stack* pst);
//栈的元素个数
int StackSize(Stack* pst);

//栈的初始化
void StackInit(Stack* pst)
{
	assert(pst);
	
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}

//栈的销毁
void StackDestory(Stack* pst)
{
	assert(pst);
	free(pst->a);
	pst->a = NULL;
	pst->capacity = pst->top = 0;
}

//压栈
void StackPush(Stack* pst, STDataType val)
{
	assert(pst);

	//检查扩容
	if (pst->top == pst->capacity)
	{
		int new_capacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
		STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * new_capacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		pst->a = tmp;
		pst->capacity = new_capacity;
	}
	
	//入数据
	pst->a[pst->top] = val;
	pst->top++;
}

//出栈
void StackPop(Stack* pst)
{
	assert(pst);
	assert(!StackEmpty(pst));

	pst->top--;
}

//判空
bool StackEmpty(Stack* pst)
{
	assert(pst);
	return pst->top == 0;
}

//取栈顶元素
STDataType StackTop(Stack* pst)
{
	assert(pst);
	assert(!StackEmpty(pst));

	return pst->a[pst->top-1];
}

//栈的元素个数
int StackSize(Stack* pst)
{
	assert(pst);

	return pst->top;
}

代码实现:

//快排的非递归
void QuickSortNonR(int* nums, int begin, int end)
{
	Stack st;
	StackInit(&st);
	//先入左边界
	StackPush(&st, begin);
	StackPush(&st, end);

	while (!StackEmpty(&st))
	{
		//先取右边界
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		if (left >= right)
			continue;

		int mid = Part1Sort(nums, left, right);

		//将划分的左右区间分别入栈
		StackPush(&st, mid + 1);
		StackPush(&st, right);

		StackPush(&st, left);
		StackPush(&st, mid - 1);
	}

	StackDestory(&st);
}
	StackDestory(&st);
}

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

为什么要先从最右边出发?

为什么快排的三种方法都是从最右边出发呢?最左边不行吗?
其实这个问题与怎么确保划分左右区间时能保证左区间的数恒小于右区间的数的问题挂钩;只有从右边出发,你才能保证左区间的数恒小于右区间的数;
原因如下:
在划分区间时,会有两种情况出现(在其中途两指针便相遇停止):1.left指针去撞right指针;2.right指针去撞left指针;第一种情况:当left指针去撞right指针时,right指针停留在比基准值小的数上(right指针后面的数都是大于基准值的数,right找小),在left指针撞上它后(其前面全部都是小于基准值的数,因为left找大),就将基准值与它进行交换,确保左边都是小于基准值的数,右边都是大于基准值的数;第二种情况:当right指针去撞left指针时,left指针停留在小于基准值的数上(其左边都是小于基准值的数),right指针撞上left说明right的右边都是大于基准值的数,在相撞后进行交换,也同样确保了左边都是小于基准值的数,右边都是大于基准值的数。

归并排序

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

八大排序算法_第11张图片

递归实现

基本思想:
将一个数组划分为两个子数组,再通过递归的方式将子数组继续划分,直到子数组中只有一个元素就返回;后对两个子数组进行归并,借助辅助数组,将两个子数组中小的数添加到辅助数组中,直到其中一个数组将最后一个元素添加到辅助数组中停止,再将另一个数组元素插入到辅助数组中;最后将辅助数组中的有效数据拷贝到原数组对应位置

代码实现:

//归并排序实现
void _MergeSort(int* nums, int left, int right, int* tmp)
{
	//如果子数组只有一个元素,则不需要继续分割
	if (left >= right)
		return;

	int mid = (left + right) / 2;
	//递归,分割左右子数组
	_MergeSort(nums, left, mid, tmp);
	_MergeSort(nums, mid + 1, right, tmp);

	
	int begin1 = left, end1 = mid;//左子数组
	int begin2 = mid + 1, end2 = right;//右子数组
	//辅助数组必须从left出发,才能对应上原数组当前的子数组
	int i = left;

	//如果一个数组走到尾,就结束
	while (begin1 <= end1 && begin2 <= end2)
	{
		//将小的添加到辅助数组中
		//左数组元素如果等于右数组元素,将左数组的数优先添加到辅助数组中,
		//可以确保稳定性,因为左数组的元素相对右数组元素靠前
		if (nums[begin1] <= nums[begin2])
		{
			tmp[i++] = nums[begin1++];
		}

		else
		{
			tmp[i++] = nums[begin2++];
		}
	}

	//其中一个数组结束,另外一个数组剩余元素全部添加
	while (begin1 <= end1)
	{
		tmp[i++] = nums[begin1++];
	}

	while(begin2 <= end2)
	{
		tmp[i++] = nums[begin2++];
	}

	//必须从left位置开始拷贝,才能保证拷贝位置与归并左右子数组相对应
	memcpy(nums + left, tmp + left, sizeof(int) * (right - left + 1));
}

//归并排序
void MergeSort(int* nums, int numsSize)
{
	//创建辅助数组
	int* tmp = (int*)malloc(sizeof(int) * numsSize);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	_MergeSort(nums, 0, numsSize - 1, tmp);
	free(tmp);
}

非递归实现

归并排序的非递归实现,相比快速排序的非递归有些许不一样。快排递归实现时采用的是前序遍历的方式,用栈模拟起来相较简单;但归并递归实现时采用的是后序遍历的方式,使用栈或队列都不易实现,所以我们得另寻它法;
从上面的动图中,我们可以发现,排序时,总是先两两归并,再四四归并,最后再总的归并;我们可以依据这一点,先设置gap为2,每gap个数据为一组进行归并,归并完毕后让gap乘2,继续让每gap个数据为一组归并,直至gap大于元素个数为止;

注意:
上述方法,只能对数组元素个数为2^N个时才能完整实现,否则将会出现数组越界。在不是2 ^N个时,将会出现以下三种情况:1.end1会越界,即左数组不完整,没有右数组;2.begin2越界,即左数组完整,没有右数组;3.end2越界,即左数组完整,右数组不完整;

分别对以上三种情况进行特殊处理,就能完整解决归并非递归问题;下面是对三种情况的解决方法:
1.end1越界,直接不进行归并,让其作为单独的数组存在(它本身已是有序,因为在其不完整之前,已经归并过了),等到它作为右数组时再进行第三种处理方式;
2.begin2越界,直接不行归并;左数组同样是已经有序的数组,让其与其他左数组进行归并;
3.end2越界,需要调整右数组边界,即end2的值;让其退回右数组的最大右边界,再进行归并;

代码实现:

//归并非递归
//整个数组有效数据一起拷贝
void MergeSortNonR(int* nums, int numsSize)
{
	int gap = 1;
	int* tmp = (int*)malloc(sizeof(int) * numsSize);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	while (gap < numsSize)
	{
		//如果是整个数组一起拷贝,就需要记录下拷贝数组的结束位置
		//如果是一个子组一个子数组拷贝,就不需要记录
		int flag = 0;
		//每gap个数组为一组
		for (int i = 0; i < numsSize; i += 2*gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int j = i;

			//左数组不完整,直接退出归并
			if (end1 >= numsSize)
			{
				flag = begin1;
				break;
			}

			//没有右数组,退出
			if (begin2 >= numsSize)
			{
				flag = end1;
				break;
			}

			//右数组越界,调整右边界
			if (end2 >= numsSize)
			{
				end2 = numsSize - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (nums[begin1] <= nums[begin2])
				{
					tmp[j++] = nums[begin1++];
				}
				else
				{
					tmp[j++] = nums[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = nums[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = nums[begin2++];
			}
		}
		
		int size = numsSize;
		if (flag != 0)
		{
			size = flag;
		}
		//整个数组的有效数据一起拷贝
		memcpy(nums, tmp, sizeof(int) * size);
		gap *= 2;
	}
}


//归并非递归
//每归并完一小组就拷贝
void MergeSortNonR(int* nums, int numsSize)
{
	int gap = 1;
	int* tmp = (int*)malloc(sizeof(int) * numsSize);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	while (gap < numsSize)
	{
		for (int i = 0; i < numsSize; i += 2*gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int j = i;

			//左数组不完整,直接退出归并
			if (end1 >= numsSize)
			{
				break;
			}

			//没有右数组,退出
			if (begin2 >= numsSize)
			{
				break;
			}

			//右数组越界,调整右边界
			if (end2 >= numsSize)
			{
				end2 = numsSize - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (nums[begin1] <= nums[begin2])
				{
					tmp[j++] = nums[begin1++];
				}
				else
				{
					tmp[j++] = nums[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = nums[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = nums[begin2++];
			}

			memcpy(nums + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
}

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

非比较排序——计数排序

基本思想:
计数排序又称鸽巢原理,是对哈希直接定址法的变形应用。操作步骤:
1.统计相同元素出现的次数;
2.根据统计的结果将序列回收到原来的序列中;

代码实现:

//计数排序
void CountSort(int* nums, int numsSize)
{
	//先计算最大值与最小值
	int max = INT_MIN, min = INT_MAX;
	for (int i = 0; i < numsSize; i++)
	{
		if (nums[i] > max)
			max = nums[i];

		if (nums[i] < min)
			min = nums[i];
	}

	//最大值与最小值之间的元素个数,用于进行映射
	int range = max - min + 1;
	//建立哈希表
	int* CountA = (int*)malloc(sizeof(int) * range);
	if (CountA == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	//初始化哈希表
	memset(CountA, 0, sizeof(int) * range);

	//进行相对映射
	for (int i = 0; i < numsSize; i++)
	{
		CountA[nums[i] - min]++;
	}

	//根据映射结果进行排序
	int j = 0; 
	for (int i = 0; i < range; i++)
	{
		//如果映射位置不为0,则说明有数存在
		while (CountA[i]--)
		{
			nums[j++] = i + min;
		}
	}
}

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

你可能感兴趣的:(排序算法,算法,数据结构)