七大排序算法(插入排序、希尔排序、选择排序、堆排序、交换排序、快速排序、归并排序)

文章目录

1.直接插入排序

2.希尔排序(插入排序的优化)

3.选择排序

4.堆排序

5.交换排序

6.快速排序

1.左右指针法

2.挖坑法

3.前后指针法 

4.非递归栈实现快速排序

 7.归并排序


1.直接插入排序

直接插入排序是较为简单直观的排序算法,在数据内选择合适的位置,将数据插入进去。通过构建一个有序数列,之后找到合适位置进行插入。步骤如下(以升序为例)

1.先完成单趟排序的代码,从第一个位置开始,所有元素被认为已经排序。

2.将下一个位置的元素保存在tmp里,之后从后往前开始遍历。

3.如果下一个元素大于tmp,则将tmp插入到这个元素的后面,该元素移动到后一位。

4.重复步骤2、4。

5.继续下一位循环,直到整个序列结束。

动画演示:

 图片演示:

 代码实现:

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

	for (int i = 0; i < n - 1; i++)
	{
		//单趟排序
		int end = i;//每一次将end向前走一步
		int tmp = a[end + 1];
		while (end >= 0)
		{
			//比插入的数大就后移
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;

		//代码执行到此有两种情况
		//1.待插入的元素已经找到适合插入的位置,已经插入
		//2.待插入的元素比当前所有的数都要小,跳出了while循环

	}

}

 直接插入排序特点总结:

1.时间复杂度为O(N~N^2)

2.空间复杂度O(1)

3.稳定性:稳定

4.元素越接近有序,排序效率越快;反之,逆序时效率最慢

2.希尔排序(插入排序的优化)

希尔排序是对直接插入排序的优化,插入排序的特点是越有序效率越快,因此通过预排序多次分组将序列变得接近有序,这样可以优化代码效率。

步骤如下:

1.选取一个小于N的gap做为增量,将所有距离为gap的数据进行直接插入排序,然后取第二个gap做第二组的增量,重复如上步骤。

2.取gap为1时,增量为1,此时进行插入排序,既可以保证完成整段序列的排序,同时因为之前的排序,序列已经趋近有序,可以增加效率。

动画演示:

图示:

代码实现 :

//希尔排序,对直接插入排序的优化,时间复杂度在O(N^1.3-N^2)
void ShellSort(int* a, int n)
{
	assert(a);
	
	//1.gap>1相当于预排序,让数组变得接近有序
	//2.gap==1相当于直接插入排序,保证有序
	
	int gap = n;

	while (gap > 1)
	{
		gap = gap / 3 + 1;//保证最后一次gap是1
		//gap == 1 相当于一次直接插入排序

		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;

		}

		PrintArray(a, n);
   }
	
}

 希尔排序的特点总结:

1.平均时间复杂度O(N*logN)

2.空间复杂度O(1)

3.稳定性:不稳定

3.选择排序

选择排序的大体思路是遍历序列选出最小值和最大值,将最小值放置在前面,最大值放在后面,直到遍历整个序列,完成升序的排序。选择排序简单直观,无论什么数据放进去都是一样的时间复杂度,因此数据规模越小越好。

步骤如下:

1.保存开头和结尾的位置,将最大值max和min同时放在第一位,准备寻找最大值和最小值。

2.遍历序列,max和min同时出发寻找,如果遇到比max大的数,则将其赋值给max,如果遇到比min小的数则将其赋值给min,直到序列遍历完。

3.确定最大值和最小值后,将其与序列开头begin和结尾end的数进行交换,再++begin和end,直到begin>end

动画演示:

代码实现:

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

}

//选择排序
void SelectSort(int* a, int n)
{
	assert(a);

	int begin = 0;
	int end = n - 1;
	while (begin <  end)
	{
		int maxi,mini;
		maxi = mini = begin;
		//找到最大和最小值
		for (int i = begin+1; i <= end; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}

		}

		Swap(&a[begin], &a[mini]);
		//如果maxi和begin位置重合,则maxi的位置需要修正
		if (begin == maxi)
		{
			maxi = mini;
		}

		Swap(&a[end], &a[maxi]);

		begin++;
		end--;
	}

}

选择排序特点总结:

1.时间复杂度为O(N^2)

2.空间复杂度为O(N^2)

3.稳定性:不稳定

4.无论原序列是不是有序,时间复杂度都是O(N^2),都要重新选择排序。

4.堆排序

堆排序的主要思想是建堆,升序建大堆,降序则建小堆,这样能保证第一位是最大值或最小值,便于交换。

步骤如下:

1.构建一个向下调整算法,构建出大堆。

2.交换第一位和最后一位end,交换后再进行向下调整,选出最大值,end--,再进行交换,直到遍历整段序列。

动画演示:

代码实现:

//堆排序
//时间复杂度是O(N*logN)
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--;	
	}


}


//向下调整算法
void AdjustDown(int* a, int n, int root)
{
	int parent = root;
	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;

		}
		else
		{
			break;
		}

	}

}

 堆排序特点总结:

1.时间复杂度为O(N*logN)

2.空间复杂度为O(1)

3.稳定性:不稳定。使用了堆排序,因此结构不稳定。

3.堆排序对数据不敏感,无论有没有序都要进行堆排序。

5.交换排序

交换排序的核心思想是选取每一位数,与前面的数进行交换判定,如果比他大则发生交换,比他小则证明已经有序,换下一个数。

步骤如下:

1.用end记录冒泡的最终位置。

2.序列中的数据进行两两比较和交换,一直到end。

3.如果过程中没有发生交换则结束循环。

4.end--,继续冒下一趟泡。

动画演示:

代码实现:

//交换排序
//时间复杂度为O(N^2)
void BubbleSort(int* a, int n)
{
	int end = n;
	while (end > 0)
	{
		int exchange = 0;

		for (int i = 1; i < end; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		//如果一趟冒泡的过程没有发生交换,则前部分已经有序,不需要继续冒泡
			if (exchange== 0)
			{
				break;
			}

		}
		end--;
	}


}

 交换排序的特点总结:

1.时间复杂度O(N)~O(N^2)

2.空间复杂度O(1)

3.稳定性:稳定

4.对数据敏感,序列越有序效率越高。

6.快速排序

快速排序的结构类似于二叉树的结构,通过一趟排序将序列分割成两部分,以调整后的中间值key为基准,左边比他小,右边比他大,之后无限将其分割,以达到排序的目的。

1.左右指针法

步骤如下:

1.选取一个基准值,可以是最左边(begin)也可以是最右边(end)。

2.根据选取的位置确定谁先走,如果选的是左边,则右边先走,如果选的是右边,则左边先走(之后解释)。因为左边选取的是比key小的数,所以遇到比key大的数时需要停下;右边则是选取比key大的数因此遇到比key小的数时需要停下。当左右两边都停下后,就可以将二者交换。

3.重复2步骤,直到序列走完。当序列走完时begin和end相遇,把基准值赋值给当前位置。

动画演示:

代码实现:

//单趟快速排序
//综合情况看,快排的时间复杂度是O(logN*N)

//1.左右指针法
int PartSort1(int* a, int begin, int end)
{
	int midIndx = GetMidIndex(a, begin, end);//取中位数,保证不是最坏的情况,不会找到最大或 
                                               者最小的情况。
	Swap(&a[midIndx], &a[end]);

	int keyindex = end;

	while (begin < end)
	{
		//bgein先走,bgein找比key大的数,找到则停下
		while (begin < end && a[begin] <= a[keyindex])
		{
			begin++;
		}

		//end后走,end找比key小的数,找到则停下
		while (begin < end && a[end] >= a[keyindex])
		{
			end--;
		}

		//此时end走到了比key小的位置,bgein走到了比key大的位置,让二者进行交换
		Swap(&a[end], &a[begin]);

		//结束后在继续走,直到把序列走完
	}

	Swap(& a[begin], & a[keyindex]);

	return begin;
}

解释选右走左,选左走右:当基准值定为最右边的时候,需要左边先走寻找比key大的数,之后右边再走寻找比key小的值,最后两者交换。几个回合后,begin和end最终会相遇,当左边先走,那就是begin遇到end,key赋值给end的位置,这样可以保证key比左边的数都要大,比右边的数都要小,可以达成排序的目的;假如是右边先走,最后begin和end仍然相遇,但是会是end遇begin,begin此时的值比key要小,当与end相遇发生交换时,会把这个小的数交换到右边去,导致右边出现比key小的数,不能完成排序,因此必须让begin遇end才行。

左右指针的特点:

1.时间复杂度为O(N*logN)

2.空间复杂度为O(1)

2.挖坑法

挖坑法相对于左右指针法更好理解,二者殊途同归,过程也是类似。核心思路是将原先的基准值定位坑,begin从左边开始找大,填到右边的坑;end从右边开始找小,填到左边的坑;最后左右相遇,把基准值填到相遇的地方。

步骤如下:

1.确定最左或者最右为开始的坑位key

2.左边开始寻找比key大的数,找到后把其填到右边的坑位;右边开始寻找比key小的数,找到后把其填到左边的坑位。

3.在begin和end相遇的地方把key填到此处。

动画演示:

 代码实现:

//2.挖坑法
int PartSort2(int* a, int begin, int end)
{
	int midIndx = GetMidIndex(a, begin, end);
	Swap(&a[midIndx], &a[end]);

	int key = a[end];
	//最开始的坑

	while (begin < end)
	{
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		//左边已经找到比key大的坑,把begin位置的数填到右边,begin的位置形成新的坑
		a[end] = a[begin];

		while (begin<end && a[end] >= key)
		{
			end--;
		}
		//右边已经找到比key小的坑,把end位置的数填到左边,end的位置形成新的坑

		a[begin] = a[end];

	}
	//begin和end相遇,把key填到二者相遇的地方
	a[begin] = key;

	return begin;
}

挖坑法特点总结:

1.时间复杂度为O(N*logN)

2.空间复杂度为O(1)

3.思路简单直观,便于理解

3.前后指针法 

前后指针法通过定义两个指针,cur在前,prev在后,仍然需要找到一个基准值key,cur向前寻找,找到目标后和prev++交换,最后把prev和key交换。

步骤如下:

1.设置两个指针,前指针cur(赋值为开头begin的位置),后指针prev(begin-1)。再选定一个基准值key

2.当cur小于key时,cur和prev的值发生交换,prev++后会发生与cur相等的情况,此时可以不交换。当cur大于key时,prev不变cur++。

3.循环第二步,直到cur到达end时,此时将++后的prev与key交换即可。

动画演示:

代码实现: 

//3.前后指针法
int PartSort3(int* a, int begin, int end)
{
	int cur = begin;
	int prev = begin - 1;
	int keyIndx = end;

	while (cur < end)
	{
		if (a[cur] < a[keyIndx]&& ++prev!=cur)//当cur小于key时,cur和prev发生交换,prev++后会 
                                                发生与cur相等的情况,此时可以不交换
		{
			Swap(&a[cur], &a[prev]);
		}

		cur++;

	}
	Swap(&a[++prev], &a[keyIndx]);

	return prev;
}
4.非递归栈实现快速排序

递归改非递归的方法大概分为两种,一种是改为循环,一些简单的递归是可以改成循环实现,另一种方式是改为由栈实现,利用栈的存储来模拟递归。

步骤如下

1.构建栈,将最右侧和最左侧依次压入栈内

2.进入循环,如果栈是非空则开始循环

3.每次循环要取出子区间,将子区间再循环处理

4.销毁栈

代码实现

void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);

    //入栈
	StackPush(&st, right);
	StackPush(&st, left);

	while (!StackEmpty(&st))
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end= StackTop(&st);
		StackPop(&st);

		//此时取出[begin,end]
		int div = PartSort3(a, begin, end);
		//[begin,div-1] div [div+1,end]

		//先处理右
		if (div + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, div+1);
		}

		//再处理左
		if (begin < div - 1)
		{
			StackPush(&st, div-1);
			StackPush(&st, begin);
		}

	}

	StackDestory(&st);
}

 7.归并排序

归并排序与合并有序数组的思想类似,将数组中的数据依次拿下来,直到数据为1,然后与其他数排序,当成两个数组合并,最后归并到一起,完成排序

1.拆分区间,将序列拆分成左右两个区间,确定先归并哪边

2.递归拆分的过程,直到序列被拆分成一个一个的

3.开始归并,按合并两个有序数组那样进行排序,利用开辟的一块空间容纳排序的序列数据,之后再拷回原序列

图示说明:

 动画演示:

代码实现:

void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int* tmp)
{
	int left = begin1, right = end2;
	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再拷回原数组
	for (int i = left; i <= right; i++)
	{
		a[i] = tmp[i];
	}

}



//时间复杂度是O(N*logN)
//空间复杂度O(N)
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return;
	}

	int mid = (left + right) / 2;
	//[left,mid][mid+1,right]有序则可以合并,现在没有序,变成子问题解决

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

	MergeArr(a, left, mid, mid + 1, right,tmp);

}

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

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

	
	free(tmp);
}

归并排序特点总结:

1.时间复杂度是O(N*logN)
2.空间复杂度O(N)

3.类似于二叉树的结构,数据稳定

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值