【数据结构】--常见的七大经典排序

【数据结构】 常见的七大经典排序

引言:作为数据结构又一大经典必考面试题,手撕排序算法题是很多人的噩梦,今天我们来梳理探索一下这七大经典算法。明其理,熟其用,穷其变。

排序:排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。分内部排序和外部排序,若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。(来自百度百科)

稳定性:所谓稳定性指的是两个想等元素,在排序过程发生之后,两者的先后位置没有发生改变.
在这里插入图片描述
①直接插入排序

  • 思想:每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。 第一趟比较前两个数,然后把第二个数按大小插入到有序表中;
    第二趟把第三个数据与前两个数从后向前扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。
    直接插入排序是由两层嵌套循环组成的。外层循环标识并决定待比较的数值。内层循环为待比较数值确定其最终位置。直接插入排序是将待比较的数值与它的前一个数值进行比较,所以外层循环是从第二个数值开始的。当前一数值比待比较数值大的情况下继续循环比较,直到找到比待比较数值小的并将待比较数值置入其后一位置,结束该次循环。

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

在这里插入图片描述

代码如下

void InsertSort(int* a, int n)//直接插入排序
{
	for (size_t 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];//插入到比他大和小的合适位置   //Swap(&a[end + 1], &a[end]);
				--end;				
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;///插入的位置为-1时
	}
}

②希尔排序

  • 思想:希尔排序是直接插入排序的优化版本,先选定一个整数,把待排序数组中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

1.时间复杂度:O(N1.3—N2)
2.稳定性:不稳定

在这里插入图片描述

void ShellSort(int* a, int n)//希尔排序
{
	int gap = n-1;
	while (gap > 1)
	{
		gap = gap / 3 + 1; //不断地缩小区间值
		for (size_t 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;
		}
	}	
}

③选择排序

  • 思想:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。

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

在这里插入图片描述

void AdjustDown(int *a, int size, int root) //向下调整算法,升序建大堆,降序建小堆
{
	int parent = root;
	int chrild = parent * 2 + 1;
	while (chrild < size)
	{
		if (chrild + 1 < size && a[chrild] < a[chrild + 1])
		{
			++chrild;
		}
		if (a[chrild] > a[parent])
		{
			Swap(&a[parent], &a[chrild]);
			parent = chrild;
			chrild = parent * 2 + 1;
		}
		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;
	}
}

④堆排序

  • 思想:堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。此处需要用到向下调整算法,需要注意的是排升序要建大堆,排降序小堆

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

在这里插入图片描述
代码如下

void AdjustDown(int* a, int size,int root)//向下调整算法,升序,建大堆(向下调整算法)
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 <size && a[child + 1] < a[child])
		{
			++child;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			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, n, i);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0],&a[end]);//将最小的数换到最顶部
		AdjustDown(a, end, 0);
		end--;
	}
}

⑤冒泡排序

  • 思想:冒泡排序是一种较为简单的排序,因为他的简单粗暴导致它的算法特性差。以升序为例,就是重复遍历数据,比较前后两个元素的大小,把两者大的一个交换到后面,直到把最大的元素调到最后边,最小的调到最前边为止。

时间复杂度:T(n)=O(n²)
空间复杂度:S(n)=O(1)
稳定性: 稳定排序

void BubbleSort(int* a, int n)//冒泡排序(升序)
{
	int end = n - 1;
	while (end>=0)
	{
		int flag = 0;
		for (int i = 0; i < end; ++i)
		{		
			if (a[i]>a[i + 1])
			{
				flag = 1;
				Swap(&a[i], &a[i + 1]);
			}
		}
		if (flag == 0)//若循环走完flag为0,则表示该数列有序
		{
			break;
		}
		--end;
	}
}

⑥快速排序

  • 思想:(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。 (2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
    (3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
    (4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

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

三种改进版本

  • hoare版本
  • 挖坑法
  • 前后指针版本

在这里插入图片描述
三种快排代码如下:

int GetMid(int* a, int left, int right)//(三数取中)
{
	assert(a);
	int mid = left + (right - left) / 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 = GetMid(a, left, right);
	Swap(&a[mid], &a[right]);
	int key =right;
	while (left < right)
	{
		while (left < right && a[left] <= a[key])//左边找比key大的数
			++left;

		while (left < right && a[right]>=a[key])//右边找比key小的数
			--right;

		Swap(&a[left], &a[right]);
	}
	Swap(&a[key], &a[left]);//当left和right相遇时,交换key和left
	return left;
}

void QuickSort1(int* a, int left, int right)
{
	if (right <= left)
		return;
	int index = PartSort1(a, left, right);//递归思想
	QuickSort1(a, left, index - 1);
	QuickSort1(a, index + 1, right);

}


int PartSort2(int* a, int left, int right)//挖坑法
{
	int key = a[right];//用key记住这个位置
	while (left < right)
	{
		while (left < right && a[left] <= key)//左边找比key大的
			++left;
		a[right] = a[left];//找到这个比key大的数据,填到key所在的位置,空出这个位置

		while(left < right && a[right] >= key)//右边找比key小的
			--right;
		a[left] = a[right]; //找到这个比key小的数据,填到left所在的位置,空出这个位置
	}

	 a[left] = key;//当left和right相遇时,把key填到left位置
	return left;
}

void QuickSort2(int* a, int left, int right)
{
	if (right <= left)
		return;
	int index = PartSort2(a, left, right);//递归思想
	QuickSort2(a, left, index - 1);
	QuickSort2(a, index + 1, right);

}

int PartSort3(int* a, int left, int right)  //左右指针法
{
	int cur = left;
	int prev = left - 1;
	int key = a[right];
	while (cur< right)
	{
		if (a[cur] < key  && ++prev != cur)
		{
			
			Swap(&a[prev], &a[cur]);
		}
		++cur;
	}

	++prev;
	Swap(&a[prev],&a[right]);

	return prev;
}

void QuickSort3(int* a, int left, int right)
{
	if (right <= left)
		return;
	int index = PartSort3(a, left, right);//递归思想
	QuickSort3(a, left, index - 1);
	QuickSort3(a, index + 1, right);
}

⑦归并排序

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

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

在这里插入图片描述
代码如下

void _MergeSort(int* a, int left, int right, int* tmp)//归并排序
{
	if (left == right)
		return;
	int mid = left + (right - left) / 2;
	// [left, mid] [mid+1, right]分别有序
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	// a[left, mid] a[mid+1, right]归并到tmp[left, right]
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int i = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1];
			++begin1;
		}
		else
		{
			tmp[i++] = a[begin2];
			++begin2;
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1];
		++begin1;
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2];
		++begin2;
	}
	memcpy(a + left, tmp + left, sizeof(int)*(i - left));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)* n);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
}

发布了45 篇原创文章 · 获赞 271 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/L19002S/article/details/102858394