【数据结构初阶】八大排序算法

一、插入排序
1.直接插入排序

1.直接插入排序思想: 假设现在已经有一个有序序列,如果有一个数字插入到这段序列的末尾,我们会选择拿这个数和它前面的每个数字都比较一遍,如果前面的数字比他大,那我们就让前面的数字赋值到这个被插入的数字位置,依次与前面的数字进行比较,最后我们把插入有序序列的数字放到他应该在的位置

void InsertSort(int* arr, int n)
{
for (int i = 0; i < n-1; i++)
{
int end = i;//end是指有序序列最后一个数字的下标
int tmp = arr[end+1];//需要暂存一下尾部位置的元素
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
end–;
}
else
{
break;
}
}
arr[end+1] = tmp;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2.代码细节说明:
一般来说,我们还是习惯将end定义为有序序列的最后一个数字的下标,如果我们将它定义为被插入数字的下标的话,比较的时候,我们就得往前比,end-1和end比,如果你喜欢这样定义的话,那也可以,后面的希尔排序,你也这样定义,比较的时候拿end-gap和end比较,也可以,没什么问题。
但是我个人觉得还是有些别扭的,我还是推荐大家将end定义为有序序列最后一个数字的下标,这样在逻辑思考上面还是对我们很有帮助的。

2.希尔排序

1.希尔排序思想: 将一整组数据分成gap组,我们先将这gap组进行组排序,将每组排好之后,可以让较大的数据尽快到后面,较小的数尽快到前面,然后我们逐渐减小gap直到1,进行最后的一次直接插入排序,最后的这次排序进行完毕,我们就可以得到一个有序序列了。

void ShellSort(int* arr, int n)//我的希尔排序一点问题都没有
{
int gap = n;
while (gap > 1)//当while循环里面gap为1时,排序一次之后,循环结束,排序结束
{
gap = gap / 3 + 1;//保证我们的最后一次排序为直接插入排序,而不是预排序
for (int i = 0; i < n - gap; i++)//实现gap组并排,并且最后一次排序为直接插入排序
{
int end = i;//end为最后一个有序数字的下标
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tmp;
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2.代码细节说明:
a. gap其实就是间距,希尔排序其实就是直接插入排序基础上的一种升华,gap/3+1是为了保证最后一次gap为1,而不是0.
如果你对于直接插入排序理解较好的话,那其实希尔排序也是很好理解的,我们只不过将数据进行了一个分组,然后进行组排序,这样会很节省时间,遍历数据的个数也会减少下来,等到最后一次排序的时候,我们也无需遍历大量的数据了,只需遍历个别不满足升序的数据即可,我们可以看到组成希尔排序的每个部分所消耗的时间其实都是短的,这也就是希尔排序效率高的一个原因,因为它可以很快的将数据归位,并且每次归位的时间消耗相比于直接插入排序,要优化很多,这也是希尔排序效率高的原因所在。
希尔排序=预排序+直接插入排序

3.希尔排序的时间复杂度:
分成gap组,每组N/gap个数据。
每组最坏的情况下的挪动次数:1 + 2 + 3 + …… + N / gap - 1 等差数列
总次数:(1 + 2 + 3 + …… + N / gap - 1) * gap次。这个次数没算外层的while循环

最开始gap很大:(N / 3 + 1),代入到总次数可得(1 + 2 + …… 3)* (N / 3 + 1),很接近于O(N)

……
快结束时,gap很小,带入到总次数可得总次数接近于等差数列,应该是O(N²),但其实并不是,因为这是最后的一次预排序,接近有序,所以我们可以近似为O(N)

我们可以将每一次的预排序的运行次数看作O(N)

N/3/3/3/3……/3/3/3=1
N/2/2/2/2……/2/2/2=1
所以预排序的次数是logN,2和3作为log的底数我们不管。所以次数就是logN
那我们其实就可以毛估出来希尔排序的时间复杂度大概是O(N*logN)这个级别的。

但希尔排序的时间复杂度并不是我们毛估出来那样的,其实它的计算难度非常大,因为随着gap的减小,总次数左右两边都是较为趋近于N的,这个时候,他的时间复杂度其实是不可以看作O(N)这类的了,我们这里给出希尔排序时间复杂度的结论,O(N^1.3),有关的严格数学证明,大家可以自己网上搜索或查阅资料

排序这个地方的时间复杂度有两个量级:一个是O(N^2),一个是O(N*logN),希尔排序,堆排序,快排,归并排序,等都是重量级的排序,自然学习的难度也是较大。

二、选择排序
1.直接选择排序
1.直接选择排序思想: 我们每次将数组遍历一遍,每遍历一遍就选出最大和最小的数字将其放在序列的首尾,直到将序列遍历完毕或者遍历的只剩一个数字时,我们的序列也就变得有序了。

void SelectSort(int* arr, int n)
{
//选出来最大和最小的数分别放在左右两端
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin; i <= end; i++)
{

		if (arr[i] < arr[mini])
		{
			mini = i;
		}
		if (arr[i] > arr[maxi])
		{
			maxi = i;
		}
	}
	if (maxi == begin)

//我们这里需要做一下调整,因为有可能最大元素的位置在begin的位置,这样我们以交换元素之后,maxi的位置就是最小元素了。
//所以如果maxi在begin的位置上,我们就需要事先把maxi的下标改为mini的下标,这样在交换元素过后,mini正好就是最大元素了。
maxi = mini;
Swap(&arr[begin], &arr[mini]);
Swap(&arr[end], &arr[maxi]);
//下面那次不用调整的原因是,上面调整过后,maxi位置肯定不存在最小元素的情况,所以不存在交换最大元素和end位置时,出现最小元素在end
//位置,因为我们是先交换begin和mini,所以下面再次交换时,肯定是不会出问题的,因为我们上面已经交换过了。

	begin++;
	end--;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2.代码细节说明:
如果出现最大元素下标正好在left位置的时候,我们第二次交换max和right一定会出问题,所以我们需要对这样的情况做出调整,将max的下标改为min的下标。
当然也有人或许会有疑问,那最小元素如果出现在right位置时,你不需要做出调整么?答案是:不需要做出调整,只需要调整一次就够了。
因为我们交换元素的顺序是先交换小在交换大,所以只要交换小不出问题,后面的交换大肯定也不会出问题。

2.堆排序(已经建好堆的基础之上)
1.堆排序思想: 升序建大堆,降序建小堆,不断的利用向下调整算法将堆顶元素依次放到堆尾。记得父节点和子节点之间的关系:parent=(child-1)/2不要给忘了。我们每重新调整堆时,传给向下调整算法的数组个数要减1,因为我们交换一次,数组的最后一个元素已经变得有序了,不需要我们调整了。

—————————————————————————————————————

void AdjustDownTeach(HPDataType* array, int n, int parent)
{
int child = parent * 2 + 1;//上来我们就先假设最大的孩子是左孩子

while (child<n)//我们想的是循环结束的条件,写的是循环继续的条件
{
	//保证有右孩子的同时,看看我们的假设是否正确,错误就调整
	if (child + 1 < n && array[child + 1] > array[child])
	//如果假设错误,我们将孩子改为右孩子,并且你也有可能没有右孩子,没有右孩子,默认左孩子就是最大的
	//这里其实不用担心没有孩子的问题,因为如果parent没有孩子,child被赋值过后肯定大于n了直接跳出循环了就。
	{
		++child;//将下标自增,左孩子就变为右孩子
	}
	if (array[parent] < array[child])//我们这里建的是大堆
	{
		Swap(&array[parent], &array[child]);
		parent = child;
		child = parent * 2 + 1;//这里再重新假设左孩子是大的,下一次循环就是先看看我们的假设是否正确,若不正确就进行调整。
	}
	else
	{
		break;
	}
}

}
void HeapSort(int* arr, int n)
{
for (int i = 1; i < n; i++)
{
Swap(&arr[0], &arr[n - i]);
AdjustDownTeach(arr, n - i, 0);//我们传过去的数组大小是需要逐渐减小的,所以传的是n-i
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2.代码细节说明:
a. 无论是建堆还是堆排序,其核心主要还是在于向下调整算法,既能帮助我们建堆,又能帮助我们对堆进行排序。我们主要说明一下向下调整算法的代码细节。

b. 由于每次判断左孩子和右孩子哪个大很费时间,并且代码写起来不够精炼,所以这里利用了一个代码技巧,就是上来就假设左孩子是最大的,如果我们假设错误,那就重新进行调整,在调整的那部分代码,有一个很坑的地方就是,有可能父节点没有右孩子,此时在if语句的条件判断部分就出现了越界访问的问题,所以我们还要加上一个判断条件,就是保证有右孩子的同时,去看一下我们的假设是否正确。

c. 另外有关于向下调整算法什么时候向下调整结束,我们这里也要说一下,一定是child>=n时,循环就要结束了,说一下为什么child==n循环就结束就可以了,因为child>n循环肯定是要结束的,这个应该是很好理解的,我也就不多说了。

三、交换排序(Swap)
1.冒泡排序(大学牲最熟悉的排序)
1.冒泡排序思想: 每遍历一次序列就将最大的元素交换到序列的最后面,直到遍历的序列中元素仅剩1个时,冒泡排序结束。

void BubbleSort(int* arr, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 0;
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
exchange = 1;
}
}
if (exchange == 0)
{
break;//数组元素已经有序,不用继续排序了,跳出循环即可。
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2.代码细节说明: 就简单的做了一个优化,如果我们排序的某一趟每个元素都不用交换,则说明这一趟要排序的元素已经有序,那后面的每趟排序其实就不用进行了,我们选择直接跳出循环。

2.快速排序(The fastest sort of all sorts有点儿装B,但确实挺快)
2.1 hoare版本
1.hoare排序思想: 我们默认序列左起第一个数为key,我们定义两个下标left和right分别从序列的左边和右边去找值,left找比key大的值,right找比key小的值,找到之后,交换left和right的值,等到left和right相遇的时候,此时的值一定是比key小的值,我们再把key和这个相遇位置的值进行交换,这样key就回到它应有的正确位置上,我们再递归处理key的左边区间和右边区间,递归结束之后,快排也就结束了。

int PartSort1(int* arr, int left, int right)
{
//1.默认左边的数为key值
//2.先让右边的数走,找小。再让左边的数走,找大,交换两个元素
//3.当两边相遇的时候,相遇位置一定是比key要小的值,交换key和相遇位置

int key = left;
while (left<right)
{
	while (left < right && arr[right] >= arr[key])
	{
		right--;
	}
	while (left < right && arr[left] <= arr[key])
	{
		left++;
	}
	Swap(&arr[left], &arr[right]);
}
Swap(&arr[key], &arr[left]);
key = left;
return key;

void QuickSort(int* arr, int begin, int end)
{
if (begin>=end)//递归结束条件
{
return;
}
int key = PartSort1(arr, begin, end);

QuickSort(arr, begin, key - 1);
QuickSort(arr, key + 1, end);

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2.代码细节说明:
代码思路看起来比较简单,其实实现的时候,有很多坑人的地方。
a. 如果key后面的每个数都比key小或大的话,那left向后面找或right向前面找,就会产生越界访问的问题,为了防止这样情况的产生,我们选择在if语句的判断部分加个逻辑与&&保证left小于right,以免产生越界访问的问题。
b. 我们在if语句的判断部分,找的数一定得比key小或大,连相等都是不可以的,为什么呢?因为会产生死循环。
一旦序列中的左右出现相等的数字的时候,我们if语句如果写成>或<而不是>=或<=,程序就废了。

2.2 三数取中+小区间优化

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值