排序算法总结


排序

在这里插入图片描述


1.插入排序

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

动图演示
20210223174254141
直接插入排序特性
a:元素越接近有序,直接插入排序算法的时间效率越高
b:时间复杂度: O(N^2)
c:空间复杂度: O(1)
d:稳定性 :稳定

代码

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)//小于n-1是为了防止 最后一次end+1越界
	{
		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;
	}
}

以上述代码为例,每次循环运行的结果(a内元素分别为9 6 3 2 8)
在这里插入图片描述


2.希尔排序

希尔排序基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量(间距)逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,排序便终止。

在这里插入图片描述

希尔排序特性
a:序是对直接插入排序的优化
b:当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样排序就会得到优化
c:稳定性: 不稳定
d:时间复杂度 O(n^1.3)

void ShellSort(int* a, int n)
{
	int gap = n ;
	while (gap > 1)
	{
		gap /= 2;
		//或者是  gap=gap/3+1;这个更好
		for (int i = 0; i < n - gap; i++)
      
		{
			int end = i;
			// i<n-gap 是为了防止  end+gap的越界
			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不变的情况下是什么样子的
假设gap=3,代码就变成了

  int gap = 3;
		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;
		}

还是有点不好理解那我们就可以再改变一下

 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;
			}
		}

这里 结合给的图理解 j控制的循环的作用就是控制了 蓝 红 绿 三组都能进行预排序 而i控制的循环是对每组分别进行排序 也就是说第三段代码表达的意思就是 “分好组之后再挨着对每组进行排序”

有了对第三段代码的初步理解之后,我们再来看第二段代码i虽然每次加1 但是它还是间距为gap的关键字进行比较 那么第二段代码表达的意思就是 “边分组边比较” 也就是 “大乱炖”
需要注意的是 两段代码并没有效率上的区别, 第三段代码 并不会因为三层循环效率就低

用不同的gap值进行排序,都会让数据更加接近有序, 第一段代码表达的就是这个意思 ,而为什么每次都除以二呢 ,是为了保证最后一次一定是1 当gap是1的时候就是直接插入排序 通过预排序 数据已经基本有序 所以此时直接插入排序的效率会大大提高
在这里插入图片描述

gap的意义
a:gap越大, 大的数可以更快的到后面, 小数更快到前面 但是越不接近有序
b:gap越小, 数据跳动的也就越慢, 越接近有序


3.选择排序

选择排序基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
请添加图片描述
既然每次循环我们已经选择除了最大或最小的数据,那不如同时把两者记录下来

void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
	//用来记录下标的位置
		int min = begin;
		int max = begin;
		
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[min] > a[i])
			{
				min = i;
			}
			if (a[max] < a[i])
			{
				max = i;
			}
		}
		swap(&a[min], &a[begin]);
		//这里多了一个判断是因为 max最大值刚好在begin的位置上,交换后max去了min的位置上去  所以要判断一下
		if (begin == max)
			max = min;
		swap(&a[max], &a[end]);
		begin++;
		end--;
	}
}

选择排序特性
a:时间复杂度 O(n^2)
b:空间复杂度 O(1)
c:稳定性 :不稳定
选择排序的时间复杂度不同于 直接插入排序 选择排序的时间复杂度并不会因为数据有序而降低 所以我们很少用到这个选择排序
而直接插入排序 对于局部有序 效率都会提升。


4 堆排序

堆排序基本思想
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为
小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

请添加图片描述
代码

void AdjustDown(int* a,int n,int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
			child++;
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
	
}
void HeapSort(int *a , int n)
{
	for (int i = (n-2) / 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--;
	}

}

堆排序分为两个部分: 建堆和排序重新建堆
我们要想理解堆排序算法就要先理解什么是向下调整和向上调整
在这里插入图片描述
在这里插入图片描述
既然向上,向下调整都能建堆,我们为什么要用向下调整的方法呢 ,因为向下调整建堆时间复杂度要优于向上调整
证明
在这里插入图片描述
同理向上调整的时间复杂度为
在这里插入图片描述
第二步排序重新建堆是指 堆顶的元素先和堆的最后一个元素进行交换,然后新的堆顶的元素再进行向下调整,这是对于逻辑结构来说的
我们的操作都是在数组即物理结构中 我们以排升序建大堆为例:数组第一个元素a[0]此时是最大的,和数组中最后一个元素进行交换 那么此时数组最后一个元素就是最大的 然后对堆顶元素向下调整 (这时候因为数组最后一个元素已经排好了,所以在调整的时候我们要把他 “去掉”) ,这时堆顶元素就是次大的 ,再和倒数第二个元素进行交换 , 以此类推,这就是排序重新调整的过程

堆排序特性
a: 时间复杂度:O(N*logN)
b: 空间复杂度:O(1)
c: 稳定性:不稳定


5.冒泡排序

代码

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

冒泡排序特性
a:时间复杂度:O(N^2)
b:空间复杂度:O(1)
c:稳定性:稳定


6.快速排序

快速排序基本思想
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
时间复杂度:O(N*logN)
空间复杂度:O(logN)

hoare版本

代码

void QuickSort(int* a, int left ,int right)
{
	int l = left;
	int r = right;
	int key = a[left];
	if (left > right)
		return;
	while (l < r)
	{
		while (l < r && a[r] >= key)
			r--;
		while (l < r && a[l] <= key)
			l++;
		swap(&a[l], &a[r]);
	}
	swap(&a[left], &a[r]);

	QuickSort(a, left, r - 1);
	QuickSort(a, r + 1, right);
}

在这里插入图片描述

上述过程是一个循环,循环 结束后除了保证key左边的值都比右边的值小之外,key还到了它正确的位置,即key已经排好了,那我们就需要通过递归,再去把key左右两边的值进行排序

需要注意的是
1.选取左边元素做key,需右边先走(这样保证了相遇位置比key小),右边做key,左边先走(保证了相遇位置比key要大)
2.等号不能省略(至少要存在一个等号,但是最好是两个都写上),两个都省略会导致死循环
在这里插入图片描述
对于快速排序的时间复杂度我们可以粗略的计算一下
在这里插入图片描述
针对上述数据有序的情况下我们就可以采取三数取中的优化方案。三数取中指的是在begin,mid,end位置上取出中间位置的值,取完之后我们把这个值和我们原先所选的key的位置进行交换,这样就不用再进行其他的修改了

int Getmid(int* a, int begin, int end)
{
	int mid = (begin + end) / 2;
	if (a[mid] < a[end])
	{
		if (a[begin] < a[mid])
			return mid;
		else if (a[end] < a[begin])
			return end;
		else
			return begin;
	}
	else //a[mid] > a[end]
	{
		if (a[begin] > a[mid])
			return mid;
		else if (a[end] > a[begin])
			return end;
		else
			return begin;
	}
}
void QuickSort(int* a, int left ,int right)
{
	
	if (left > right)
		return; 
	int mid = Getmid(a, left, right);
	//swap(&a[left], &a[mid]);
	int l = left;
	int r = right;
	int key = a[left];

	while (l < r)
	{
		while (l < r && a[r] >= key)
			r--;
		while (l < r && a[l] <= key)
			l++;
		swap(&a[l], &a[r]);
	}
	swap(&a[left], &a[r]);

	QuickSort(a, left, r - 1);
	QuickSort(a, r + 1, right);
}

除了上述问题之外,我们还会遇到另一个问题,这是递归版本,我们在排最后几个数的时候在这里插入图片描述
这就用到了小区间优化
我们用小区间优化理论上可以节省掉80%以上的空间,因为
在这里插入图片描述

void QuickSort(int* a, int left ,int right)
{
	
	if (left > right)
		return; 
	if ((right - left + 1) < 10)
	{
		InsertSort(a + left, right - left + 1);//注意此时的区间应该是从a+left位置开始的,因为这是被递归分割的区间
		                                       //不能从头开始

	}
	else
	{
		int mid = Getmid(a, left, right);
		//swap(&a[left], &a[mid]);
		int l = left;
		int r = right;
		int key = a[left];

		while (l < r)
		{
			while (l < r && a[r] >= key)
				r--;
			while (l < r && a[l] <= key)
				l++;
			swap(&a[l], &a[r]);
		}
		swap(&a[left], &a[r]);

		QuickSort(a, left, r - 1);
		QuickSort(a, r + 1, right);
	}
	
}

2.挖坑法

代码

void QuickSort(int* a, int left, int right)
{

	if (left > right)
		return;
	      if ((right - left + 1) < 15)
			{
				InsertSort(a + left, right - left + 1);
			}
		  else
		  {
			  int mid = Getmid(a, left, right);
			  swap(&a[left], &a[mid]);
			  int l = left;
			  int r = right;
			  int key = a[left];
			  int hole = left;
			  while (l < r)
			  {
				  while (l < r && a[r] >= key)
				  {
					  r--;
				  }
				  a[hole] = a[r];
				  hole = r;
				  while (l < r && a[l] <= key)
				  {
					  l++;
				  }
				  a[hole] = a[l];
				  hole = l;
			  }
			  a[hole] = key;
			  QuickSort(a, left, r - 1);
			  QuickSort(a, r + 1, right);
		  }
}

我们拿一个循环过程来看与hoare版本的区别
在这里插入图片描述

3.前后指针法

代码

void QuickSort(int* a, int left, int right)
{
	if (left > right)
		return;
	if ((right - left + 1) < 15)
	{
       InsertSort(a + left, right - left + 1);
	}
	else
	{
		int mid = Getmid(a, left, right);
		swap(&a[left], &a[mid]);
		int cur = left + 1;
		int prev = left;
		int key = left;

		while (cur <= right)
		{
			if (a[cur] < a[key] && ++prev != cur)//++prev!=cur是因为当两者重合的时候也不用交换了,提高效率
			{
				swap(&a[cur], &a[prev]);
			}
			++cur;
		}
		swap(&a[left], &a[prev]);
		QuickSort(a, left, prev - 1);
		QuickSort(a, prev + 1, right);
	}
	
	
}

前后指针法的精髓就在于cur找比key所在位置值小的位置,然后停下来,接着++prev 然后交换两者的值
通过一个动图来演示请添加图片描述

4.非递归版

非递归版也是根据递归版的思路来的,只不过是用栈来模拟递归的过程,像二叉树的深度遍历
我们知道通过递归,我们是分割的一个个区间, 所以我们用栈存储的应该是区间的值, 并且我们是从左到右递归的,那么根据栈的性质,存储数据的时候也应该是右侧区间先入栈

代码

void QuickSort(int* a, int begin, int end)
{
	stack<int> s;
	s.push(begin);
	s.push(end);
	while (!s.empty())
	{
		int right = s.top();
		s.pop();
		int left = s.top();
		s.pop();
		int mid = GetMidIndex(a, begin, end);
		swap(a[right], a[mid]);
		//中间的这段代码是可以替换成上面三种方法的任何一种
		int keyi = left;
		int prev = left, cur = left + 1;
		while (cur <= right)
		{
			if (a[cur] < a[keyi] && ++prev != cur)
			{
				swap(a[prev], a[cur]);
			}
			++cur;
		}

		swap(a[prev], a[keyi]);
		keyi = prev;
		//
		if (keyi + 1 < right)
		{
			s.push(keyi + 1);
			s.push(right);
		}
		if (left < keyi - 1)
		{
			s.push(left);
			s.push(keyi - 1);
		}

	}
}

5.三路划分

经典的快速排序算法都是二路划分,所谓二路划分指的就是每趟排序让比key值大的到一边,小的到另一边,这样的算法经过三数取中等各类优化之后,能够适应大部分数据,但是对于有很多重复元素的情况,它的时间复杂度又会退化成O ( N ^ 2) (因为在包含大量重复元素的情况下,相当部分的区间,会存在重复元素做key值,那么下次区间划分的时候,就会导致下个区间里面仅有一个值(就是key),剩下的元素都在另一个区间,这样我们就又回到了O ( N ^2))

为了解决两路划分的不足,就提出了三路划分的算法,三路划分就是小于key的放到一起,大于key的放到一起,等于key的放到一起.
最关键的思想就是
在这里插入图片描述

直接来看个动图理解思想
请添加图片描述
很多同学都会有个疑问,为什么第三种情况cur不用++,这是因为交换位置之后cur当前的元素还是可能比key要大,如果此时把cur++,就忽略掉了这个元素,出现错误

来看代码是怎么实现的

void QuickSortTD(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	int mid = Getmid(a, begin, end);
	
	swap(&a[begin], &a[mid]);

	int l = begin;
	int r = end;
	int key = a[l];
	int cur = begin + 1;
	while (cur <= r)
	{
		if (a[cur] < key)
		{
			swap(&a[cur], &a[l]);
			cur++;
			l++;
		}
		else if (a[cur] > key)
		{
			swap(&a[cur], &a[r]);
			r--;
		}
		else 
		{
			cur++;
		}
	}
	QuickSortTD(a, begin, l - 1);
	QuickSortTD(a, r + 1, end);
}

7.归并排序

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

归并排序的思路跟快速排序很类似,不同的是
再借助动图理解一下请添加图片描述

递归版

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 k = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[k++] = a[begin1++];
		}
		else
		{
			tmp[k++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[k++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[k++] = 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;
}

归并排序就是像上面的图一样,把每一段区间里的值排好序之后,再跟其他的区间进行合并,这样不断地合并,到最后整段区间就是有序的了
时间复杂度:O(N*logN)
空间复杂度:O(N)
实际上因为归并也递归了,空间复杂度应该是O(N+logN)
但是因为logN和N相差很大,就把logN忽略掉了

非递归版

递归版的归并排序,无非就是区间划分是通过递归来实现的,那我们通过循环来实现的话,就需要考虑怎样把区间划分好
代码

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

	int N = 1;
	while (N < n)
	{
		for (int i = 0; i < n; i += 2 * N)
		{
			int begin1 = i;
			int end1 = i + N - 1;
			int begin2 = i + N;
			int end2 = i +  2 * N - 1;


			int j = i;
			if (end1 >= n)//判断end1,begin2,end2是否越界
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;

			}
			else if (begin2 >= n)//判断begin2,end2是否越界
			{
				begin2 = n;
				end2 = n - 1;
			}

			else if (end2 >= n)//判断end2是否越界
			{
				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, tmp, sizeof(int) * n);

		N *= 2;	
	}
	free(tmp);
	tmp = NULL;
}

现在来解释一下上面写法的原理和注意事项
在这里插入图片描述

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

	int N = 1;
	while (N < n)
	{
		for (int i = 0; i < n; i += 2 * N)
		{
			int begin1 = i;
			int end1 = i + N - 1;
			int begin2 = i + N;
			int end2 = i + 2 * N - 1;
			int j = i;
			if (end1 >= n)//判断end1,begin2,end2是否越界
			{
				break;
			}
			else if (begin2 >= n)//判断begin2,end2是否越界
			{
				break;
			}

			else if (end2 >= n)//判断end2是否越界
			{
				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));
		}
		N *= 2;
	}
	free(tmp);
	tmp = NULL;
}

归并排序特性
1.时间复杂度:O(N*logN)
2.空间复杂度:O(N)
3.稳定性:稳定


总结

稳定性是指 在排序过程中保持相同元素的相对位置保持不变
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值