排序2:直接选择排序、堆排序、直接插入排序、希尔排序

1.选择排序

1. 直接选择排序:

  • 思路:每次选择最大或最小数字下标,与数组最后一个元素交换。
  • 升级版:每次选最大和最小,分别在头和尾处交换。
  • 普通版步骤:
  1. 头尾标记位:begin、end。
  2. 遍历中选最大最小
  3. a
  • 普通版代码:
void SelectSort(int* a, int n)
{
	int maxi = 0;
	for (int end = n - 1; end > 0; end--)
	{
		maxi = 0;
		for(int i = 0; i <= end; i++)
		{
			if (a[maxi] < a[i])
				maxi = i;
		}
		Swap(&a[maxi], &a[end]);
	}
}

  • 普通版代码分析:
  1. 先写内层:每次从0到末尾end,内层做加加,但是每次的end在减1,所以外层控制end。且end做减减。
  2. 外层控制次数,且控制末尾。因为每次end在减小。所以从end = n-1处到位置1处,这样最后剩下1个元素肯定有序了。
  3. maxi每次都是要从0开始往后走。
  • 升级版代码:
void SelectSortTwo(int* a, int n)
{
	int maxi = 0;
	int mini = 0;
	for (int end = n - 1, begin = 0; begin <= end; end--, begin++)
	{
		printf("%大的位%d 小的位%d\n", end, begin);
		maxi = begin;
		mini = begin;
		for (int i = begin+1; i <= end; i++)
		{
			if (a[maxi] < a[i])
				maxi = i;
			if (a[mini] > a[i])
				mini = i;
		}
		Swap(&a[maxi], &a[end]);
		if (mini == end)
			mini = maxi;
		Swap(&a[mini], &a[begin]);
	}
}
  • 升级版代码分析:
  1. 现在多了一个mini,mini放首,所以也多一个begin,因为一轮同时放begin和end。在原始基础代码上加做修改。
  2. 每一遍去找mini、maxi,所以每一轮起始初始为begin。
  3. 注意内部的起始位置和终止位置都要发生改变,且每一轮初始为begin+1。
  4. 犯过错:每次循环外要做两次Swap,如果首尾位置存在最大或最小,先交换的时候要考虑,会不会影响后面的位置,所以要加判断。,此外,我是通过边调试边画图的,F10会直接跳过某个函数。
    请添加图片描述
  • 复杂度分析:
    O(n^2):每次都要从头到尾遍历,但是每次减1,所以显然是:n+n-1+n-2+…1 = n ^2 / 2 => O(n^2)。
  • 稳定性分析:
    显然相同的数字位置会发生改变,是不稳定的。

比如对于【5、5、5、9、1】,最小在最后,1和第一个5交换后,显然第一个5和第二个第三个5的相对位置会发生变化。

2. 堆排序
前面有博客分析过,最好的堆排序时间复杂度为O(N)。做法是:默认堆已经建好【直接利用数组本身】,从最后一个父节点开始到根节点,不断做向下调整。
涉及公式总结:
用父求子:左:left = parent * 2 + 1 右:right = parent * 2 + 2
用子求父:(child - 1) / 2
求最后一个父节点:n个节点最后一个孩子是n-1,求父亲 (n-1-1) /2,所以下面最后一个父节点是:【(n-2)/2】

  • 思路:
  1. 堆排序:从最后一个父节点起:【(n-2)/2】,因为n-1是最后一个孩子,而(child-1)/2是父节点。
  2. 向下调整:
    每次的向下调整都是从爹开始,向下调整。
    求儿子位置,因为爹要和儿子做比较,如果小(大)则交换。注意要判断右儿子存在不存在。直到n-1位置。
    比较的循环范围是:minchild < n ,循环爹的最小的儿子比:左孩子一定是存在的,然后看右孩子存在不,如果右孩子存在且更小,就替换。
    min_child+1 < n:因为C语言数组错误读不会报错。
    大于就交换。
    巧妙:一旦发现a[parent] 不符合>a[min_child]就停止。但是大于就一直往下。

重要的事情再说三遍:
查右儿子是否存在:使用代码:minchild+1 < n ,因为C语言数组错误读不报错
查右儿子是否存在:使用代码:minchild+1 < n ,因为C语言数组错误读不报错
查右儿子是否存在:使用代码:minchild+1 < n ,因为C语言数组错误读不报错

  • 代码:

void AdjustDown(int* a, int n, int parent)
{
	int min_child = 2 * parent + 1;
	while (min_child<n)
	{
		if (min_child + 1 < n && a[min_child + 1] < a[min_child])
			min_child++;
		if (a[parent] > a[min_child])
		{
			Swap(&a[parent], &a[min_child]);
			parent = min_child;
			min_child = parent * 2 + 1;
		}
		else
			break;
	}
}

void HeapSort(int* a, int n)
{
	int parent = (n-1-1)/2;
	for (int i = parent; i >= 0; i--)
	{
		// 对a、n个数、从点i处做
		AdjustDown(a, n, i);
	}
}
  • 代码分析:
  1. 堆排序的分析:由于使用复杂度O(N)的建堆做法,步骤是直接从数组上操作。从最后一个父亲节点开始做向下调整。所以i范围:【(n-1-1)/2,0】,做减减。
    向下调整分析:向下调整每次都是从父parent到n-1位置,如果局部堆已经符合了大于小于关系,就停止。此外,注意需要判断右儿子是否存在且是否大于min_child,min_child默认是左孩子。
  • 复杂度分析:

  • 稳定性分析:
    堆排序不稳定,比如原本堆:【5,6,7,9,9,9,9,9】。出堆顶5后,最后一个9上去,显然即使向下调整,几个9之间的相对位置已经发生了改变。

2.插入排序

1. 直接插入排序:

  • 思路:
    默认数组0位置即第一个数字已经有序,然后不断用后面的数字跟前面比较,如果大于的都往后挪动,直到找到小于位置。
  • 代码:
    写法(一)
void insertSort(int* a, int n)
{
	int tmp = 0;
	int j = 0;
	for (int i = 1; i <= n - 1; i++)
	{
		tmp = a[i];
		for (j = i; a[j-1] > tmp && j >=0 ; j--)
		{
			a[j] = a[j - 1];
		}
		if (j < 0)
			j = 0;
		a[j] = tmp;
	}
}

  • 代码分析:
  1. 第一次比a【1】,第二次a【2】,第三次a【3】。。。每次都在变化着一直向前比,且向前最坏到a【0】,必须双重循环。外层是当前要排序的数字,内层循环是控制之前比它大的做移位。移位之前,先保存a[i]为tmp。
  2. 挪到过程中,防止错误读,给j加限制,不然j会到-1,是不存在的,但是C语言不报错。
  3. 注意移位到0,如果a【0】仍然大于当前,j已经到了-1,如果j = -1了,先给j变为0,再给a[j]。如果tmp>a[j-1],说明可以放了。但是小于的时,已经把之前都挪到过去了。当前j位置就是。
  • 代码写法(二)
void insertSort(int* a, int n)
{
	int tmp = 0;
	int j = 0;
	for (int i = 0; i <= n - 2; i++)
	{
		int end = i;
		tmp = a[end + 1];
		for (j = end; j >= 0; j--)
			if (a[j] > tmp)
				a[j + 1] = a[j];
			else
				break;
		a[j+1] = tmp;
	}
}

  • 代码分析:

给【0, end】插值: 从1开始,
因为要插当前tmp,所以先存起来tmp 【0, end】
有序区间是【0~end】,从end开始往前走到0,
注意这样的挪法,j的后一位是可用位置
i :【0,n-2】,end从0开始,默认a【0】有序了。tmp是end+1位置,每次向前都要到a【0】位。

  • 复杂度分析:
    按最坏来看,n-1+n-2+n-3+。。。+1 = n^2 / 2。
    所以时间复杂度是 o(n^2)。
  • 稳定性分析:
    显然,遇到等于情况可以让停止,所以直接插入可以实现相同数字排序过程中相对位置不变。插入排序是稳定的
    2. 希尔排序
  • 思路:

对每个以gap为间隔的数组,做排序,gap从大到小变化。gap越大,大的数越快地往后走,gap越小,越接近有序,且gap=1时,是插入排序,此时基本有序,速度会很快。gap==1时相当于做直接插入排序。

  • 代码:
void shellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i <= n - 1 - gap; i++)
		{
			int end = i;
			int tmp;
			int j;
			tmp = a[end + gap];
			for (j = end; j >= 0; j -= gap)
			{
				if (a[j] > tmp)
					a[j + gap] = a[j];
				else
					break;
			}
			a[j + gap] = tmp;
		}
	}
}
  • 代码分析:

从内向外写,先写一趟排序,再套每一趟,最后改变gap

  1. 做差为gap的数组的直接插入:
    j的范围:从end到0,对以end为尾的范围做插入,我们目的是:j判断当前位置是否大于tmp,大于就往后挪动当前位置值,之后j减gap,
    到了j位置,需要j位置有值,所以j大于0即可。
  2. 接着,写变化的end,因为需要对每个数字往前插:范围是:【0,n-1-gap】
    end变为i,i范围:【0, n-1-gap】 因为tmp记录的是每个位置的后gap个位置的值,所以0位置也需要,因为要拿0的后gap个。最后一个元素是n-1-gap,
    因为它是最后一个存在后gap的位置。i做加加,因为要对每个位置找后gap个做插入。
  3. 对gap:gap要从大到小变化,从大值变为1,必须经过1的时刻。
    gap的范围: 初始为n,做 gap = gap / 3 + 1;
    循环的条件是: gap > 1 ,
  4. 我一开始加了等于while(gap>=1),后来运行发现死循环,F11调试后发现,一直在gap==1陷进去了。所以发现,gap>1做排序即可。
  • 复杂度分析:
    gap>1时都是预排序,为了让数据更接近有序,从而提升gap==1时的排序效率。其时间复杂度不好计算,一般认为是:O(n^1.3)

  • 稳定性分析:
    在挪动过程中,相同的数值可能会分到不同数组,使得相对位置发生了改变。所以希尔排序不稳定

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值