排序算法总结

在介绍排序算法之前,先在其他小伙伴里找到一张排序算法时间复杂度,空间复杂度以及稳定性的总结图片(桶排序和基数排序先没更新),而排序算法中的稳定性是指,在排序过程后,这一组数据之间的相对位置不能发生改变,就比如说:1 2 5 2 5 3,排完序后不能把原本后面的5排到前面那个5的前面去。
在这里插入图片描述

  • 插入类排序
  1. 直接插入排序

    时间复杂度: O(n2)
    空间复杂度: O(1)
    稳定性: 稳定
    直接插入排序是把待排的数字,从后向前,依次插入一段有序的序列中,直至序列全部有序。至于为什么从后向前比较,是因为可以在比较的途中,直接把大数字向后挪移,就减少了遍历的时间,这种情况没有什么比用打牌来说明更好的了。
    在这里插入图片描述
// 插入排序
void InsertSort(int* a, int n)
{
	int i = 0;
	//i表示当前需要插入的数字的下标
	for (i = 1; i < n; i++)
	{
		int end = i - 1;//需要比较的数字的下标
		int tmp = a[i];//需要插入的数字
		while (end >= 0)
		{
			//如果前面的数比较大,把大数向后挪
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
			}
			else
			{
				//找到第一个小于或等于后面数的位置就退出循环
				break;
			}
			end--;
		}
		a[end + 1] = tmp;
	}
}
  1. 希尔排序

时间复杂度:O(n logn) ~O(n2)
空间复杂度: O(1)
稳定性: 不稳定
希尔排序又称为缩小增量排序,先选定一个距离,然后把所有间隔为该距离的数想成一组,对改组数据进行排序,重复该操作,直至距离为1,整个序列有序了。
在这里插入图片描述

// 希尔排序
void ShellSort(int* a, int n)
{
	//预排序  间距为gap 的插入排序
	int gap = n;

	//gap != 1 -->预排序阶段
	//gap == 1 --> 排序阶段
	while (gap != 1)
	{
		gap = gap / 3 + 1; //定义每次排序的间距
		int j = 0;
		for (j = 0; j < n - gap; j++)
		{
			int end = j ;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
				}
				else
				{
					break;
				}
				end -= gap;
			}
			a[end + gap] = tmp;
		}
	}
}

  • 选择类排序
  1. 选择排序

时间复杂度:O(n2)
空间复杂度:O(1)
稳定性: 不稳定
选择排序有两种写法,一种是只从一边出发,每次找到当前无序数组中最大或者最小的,然后放在最后;另一种是从两边出发,每次找到当前数组最大和最小的数字,然后放在两边。(这里需要小心,如果说最大数在最左边,那么先交换最小数,可能会出现最大数不在记录的位置)
eg: 8 1 3 5 2 4
第一遍找完,发现min = 1,max = 0(该数字的下标)
然后先把min 放到 0号下标的位置,就变成了
1 8 3 5 2 4
紧接着再把max 放到 5 号末尾,按照下标交换的结果就是
4 8 3 5 2 1
就跟我们预期的结果不同,这种情况需要单独处理。

// 选择排序
void SelectSort(int* a, int n)
{
	int i = 0;
	int j = 0;
	for (j = 0; j < n / 2; j++)
	{
		int min = j, max = j;
		for (i = j; i < n - j; i++)
		{
			if (a[i] > a[max])
			{
				max = i;
			}
			else if (a[i] < a[min])
			{
				min = i;
			}
		}
		Swap(a, j, min);
		//处理冲突的情况
		if (j == max)
		{
			max = min;
		}
		Swap(a, n - j - 1, max);
	}
	
}
  1. 堆排序

时间复杂度:O(n log(n))
空间复杂度:O(1)
稳定性: 不稳定
在进行堆排序时,首先得知道堆这个概念
堆分为两种,一种是大根堆,一种是小根堆。根节点最大的堆叫做最大根堆或大根堆,根节点最小的堆叫做最小根堆或小根堆。
堆的特征: 堆中每个节点的值总是不大于或不小于其父亲节点的值;堆总是一颗完全二叉树。既然是完全二叉树,那么堆可以用数组来存储。
在这里插入图片描述
而在堆排序中,想要按照升序来排列,需要建大堆;按照降序排列需要建小堆。
如果是升序排列,根节点的值永远为当前堆的最大值,我们每次就可以交换当前无序数组两端的数字,然后再向下建堆(此时无序数组长度 -1),这样就可以每次把最大值放到有序的位置。
应用:平常玩王者荣耀的时候,旁边总会有一个全区前100,排位前多少名的序列,这个时候,表中只需要前100名,总不可能每次更新排名的时候,都把所有玩家排个序,然后取前100名玩家。
这个时候就可以通过建立一个100个数据的小根堆,此时根节点为当前的最小值,然后只要后面的数据比根节点大,就交换根节点和改数据的值,再向下建堆,增加了效率。(广告加的一点也不生硬)

// 堆排序
//向下建立大根堆
//a 是堆的数组 n 是无序数组的长度 root 需要建堆的根节点
void AdjustDwon(int* a, int n, int root)
{
	int parent = root;//父亲节点
	int child = root * 2 + 1;//左孩子节点(因为数组的起始位置是 0 ,所以需要 +1)

	while (child < n)
	{
		//先选择出父亲节点的左右孩子节点比较大的一个节点
		if (child < (n - 1) && a[child] < a[child + 1])
		{
			child++;
		}
		
		if (a[parent] < a[child])
		{
			//如果父亲节点值小于孩子节点,交换两值
			Swap(a, parent, child);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			//如果父亲节点大于孩子节点,就不需要再判断了,大根堆的每一个根节点都是一个大根堆。
			break;
		}
	}
}
//堆排序
void HeapSort(int* a, int n)
{
	int i = 0;
	//先建立一个大根堆,向下调整法
	for (i = n / 2; i >= 0; i--)
	{
		AdjustDwon(a, n, i);
	}
	//排序
	for (i = n - 1; i > 0; i--)
	{
		Swap(a, 0, i);
		AdjustDwon(a, i, 0);
	}
}
  • 交换类排序
  1. 冒泡排序

时间复杂度:O(n2)
空间复杂度:O(1)
稳定性:稳定
冒泡排序作为我们平常使用最多的排序,代码简洁,不容易出错,还能解决很多不是大数据的问题,这里就不做解释了代码不香吗

void Bubbing(int* a,int len)
{
    int i=0;
    int j=0;
    for(i=1;i<len;i++)
    {
    	int flag = 1;
        for(j=0;j<len-i;j++)
        {
            if(a[j]>a[j+1])
            {
                int t=a[j];
                a[j]=a[j+1];
                a[j+1]=t;
                flag = 0;
            }
        }
        //如果没发生交换,说明所有序列都是有序的,不需要再继续下去了
		if(flag)
		{
			break;
		}
    }
}
  1. 快速排序

时间复杂度:O(n log(n)) ~ O(n2)
空间复杂度:O(log(n)) (递归调用函数,开辟栈帧消耗空间)
稳定性:不稳定
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
在这里插入图片描述
快速排序的三总形式:hoare版本,挖坑法,前后指针版本
快速排序的优化:三数取中,随机数法(排序速度选择权交给上帝)
快速排序在排序中,如果当前序列本身就是有序的,那么快速排序的时间复杂度就到最坏的情况,即O(n2),想要让快速排序排的越快,就跟我们的选值有关,如果我们让选的值最后都在当前序列的中间位置,就可以让他的速度相对来说快一点(三数取中)。

三数取中,随机数法

void choose(int* a,int left,int right)
{
	//随机数法
	//int key = left + rand()%(right - left);
	//三数取中
	int key = (left + right) / 2;
	
	if (a[left] < a[key])
	{
		if (a[key] < a[right])
		{
			Swap(a, left, key);
		}
		else
		{
			if (a[left] < a[right])
			{
				Swap(a, left, right);
			}
		}
	}
	else
	{
		if (a[key] > a[right])
		{
			Swap(a, left, key);
		}
		else
		{
			if (a[left] > a[right])
			{
				Swap(a, left, right);
			}
		}
	}
}

递归版本

快速排序hoare版本

如果第一个数选的是最左边的数,那么就先从右边开始找第一个比他小的,再从左边找第一个比他大的,然后交换这两个数的位置,持续这样做,直到两指针相遇,再交换最左边和相遇点的数据。

// 快速排序hoare版本
void PartSort1(int* a, int left, int right)
{
	//递归结束条件
	if (left >= right)
	{
		return;
	}
	//快速排序的优化,三数取中,随机数法
	choose(a,left,right);
	
	int key = left;
	int l = left;
	int r = right;

	while (left < right)
	{
		//找到从右向左第一个小于 a[key] 的值
		while (left < right && a[right] >= a[key])
		{
			right--;
		}

		//找到从左往右第一个大于 a[key] 的值
		while (left < right && a[left] <= a[key])
		{
			left++;
		}

		Swap(a, left, right);
	}
	Swap(a, left, key);

	PartSort1(a, l, left - 1);
	PartSort1(a, left + 1, r);
}

快速排序递归挖坑版本

挖坑法呢,顾明思议,排序前先在选择的位置留下一个坑,只要找到一个不符合的数,就用这个数来填坑,然后这个数原本的位置就变成了一个坑,留到最后给终止位置填坑。

// 快速排序挖坑法
void PartSort2(int* a, int left, int right)
{
	//递归结束条件
	if (left >= right)
	{
		return;
	}

	choose(a, left, right);

	int key = a[left];
	int l = left;
	int r = right;

	while (left < right)
	{
		//找到从右向左第一个小于 a[key] 的值
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[left] = a[right];
		//找到从左往右第一个大于 a[key] 的值
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[right] = a[left];
	}
	a[left] = key;//填坑
	PartSort1(a, l, left - 1);
	PartSort1(a, left + 1, r);
}

快速排序递归前后指针法

这个方法的思路就跟用O(n)的时间复杂度,把一个序列中偶数放在前面,奇数放在后面一样。

// 快速排序前后指针法
void PartSort3(int* a, int left, int right)
{
	//递归结束条件
	if (left >= right)
	{
		return;
	}

	int prev = left - 1;
	int cur = left;

	int key = a[left];
	int i = 0;

	while (cur <= right)
	{
		if (a[cur] < key)
		{
			Swap(a, ++prev, cur);
		}
		cur++;
	}

	PartSort1(a, left, prev - 1);
	PartSort1(a, prev + 1, right);
}

非递归版本
首先呢,递归版本其实是利用函数调用开辟的栈帧来保存数据,栈帧的本质还是栈,先进后出,我们就可以利用栈来改成非递归的版本,感觉就像是一个换了名字的深度优先搜索。

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);
	//入栈方式 先入当前区间的左边,再入右边
	StackPush(&st, left);
	StackPush(&st, right);

	while (!StackEmpty(&st))
	{
		//弹栈时,先得到的是区间的右边,后得到左边
		right = StackTop(&st);
		StackPop(&st);
		left = StackTop(&st);
		StackPop(&st);

		//去除只有一个数或者没有数的情况的情况
		if (left >= right)
		{
			continue;
		}

		int key = left;
		int l = left;
		int r = right;

		while (left < right)
		{
			//找到从右向左第一个小于 a[key] 的值
			while (left < right && a[right] >= a[key])
			{
				right--;
			}

			//找到从左往右第一个大于 a[key] 的值
			while (left < right && a[left] <= a[key])
			{
				left++;
			}

			Swap(a, left, right);
		}
		Swap(a, left, key);

		//先让右边区间入栈
		StackPush(&st, left+1);
		StackPush(&st, r);
		//左边区间入栈
		StackPush(&st, l);
		StackPush(&st, left - 1);
	}

	StackDestroy(&st);
}
  • 归并类排序
  1. 归并排序

时间复杂度:O(n log(n))
空间复杂度:O(n)
稳定性:稳定
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
在这里插入图片描述

void _MergeSort(int* a, int* tmp, int left, int right)
{
	int mid = (left + right) / 2;
	/*
	[0,7] -> [0,3] [4,7]
	递归[0,3] -> 划分[0,1][2,3]

	递归[0,1] ->划分[0,0][1,1] -->归并出有序的 [0,1]
	递归[2,3] ->划分[2,2][3,3] -->归并出有序的 [2,3]

	[0,1][2,3] --> 归并出有序的[0,3]

	递归[4,7] -> 划分[4,5][5,7]
	递归[4,5] ->划分[4,4][5,5] -->归并出有序的 [4,5]
	递归[6,7] ->划分[6,6][7,7] -->归并出有序的 [6,7]
	[4,5] [6,7] --> 归并出有序的[4,7]

	[0,3] [4,7] --> 归并出有序的[0,7]
	*/

	if (left >= right)
	{
		return;
	}

	//[left,mid]  [mid+1,right]
	_MergeSort(a, tmp, left, mid);
	_MergeSort(a, tmp, mid + 1, right);

	//两个有序数组的合并
	int begin1 = left, begin2 = mid + 1;
	int end1 = mid, end2 = right;
	int index = begin1;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}

	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}
	//将tmp排序好的数据拷贝到a数组对应位置
	memcpy(a + left, tmp + left, sizeof(int)* (right - left + 1));
}

// 归并排序递归实现
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)* n);

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

非递归版本

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)//n为数组里元素的数量
{
	int* tmp = (int*)malloc(sizeof(int)* n);

	int size = 1;
	int count = (int)(log(n) / log(2)) + 1;;
	for (; size < n;size *= 2)
	{
		int i = 0;
		for (i = 0; i < n; i += size*2)
		{
			int left = i, mid = i + size,right = mid + size;

			//处理最后一组不满足size个大小的情况
			if (mid > n)
			{
				mid = n;
			}
			if (right > n)
			{
				right = n;
			}

			//两个有序数组的合并
			//[begin1,end1) [begin2,end2)
			int begin1 = left, begin2 = mid;
			int end1 = mid, end2 = right;
			int index = begin1;
			while (begin1 < end1 && begin2 < end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 < end1)
			{
				tmp[index++] = a[begin1++];
			}

			while (begin2 < end2)
			{
				tmp[index++] = a[begin2++];
			}
		}
		// 将tmp排序好的数据拷贝到a数组对应位置
		memcpy(a, tmp, sizeof(int)* n);
	}
	free(tmp);
}
  • 非比较类排序
  1. 计数排序

时间复杂度:O(max(n,范围))
空间复杂度:O(范围)
稳定性:稳定
计数排序需要先找到当前数组的最大值和最小值,然后通过一个辅助空间,把最大值和最小值压缩在一个辅助数组中,如果最大值和最小值之间差距比较大,而且数据还是那种稀疏矩阵类型的,是用计术排序就显得不那么好。

// 计数排序(非比较类)
void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];

	//先找到数组中最大值和最小值
	int i = 0;
	for (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* arr = (int*)calloc(range,sizeof(int));

	//统计对应的数据出现的次数
	for (i = 0; i < n; i++)
	{
		int tmp = a[i] - min;
		arr[tmp]++;
	}
	int len = 0;
	//根据次数排序,返回原数组
	for (i = 0; i < range; i++)
	{
		while (arr[i])
		{
			a[len++] = i + min;
			arr[i]--;
		}
	}

	free(arr);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章