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

本文介绍了排序算法中的直接插入排序和希尔排序,详细解析了两种算法的思想和代码实现。直接插入排序通过将元素逐个插入已排序序列来构建有序序列,而希尔排序则采用增量分组的方式提高排序效率,最终通过直接插入排序完成排序。文章还探讨了希尔排序的时间复杂度,并提及选择排序、堆排序和快速排序等其他排序算法。
摘要由CSDN通过智能技术生成

一、插入排序
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 三数取中+小区间优化

Here's a possible implementation of the `fastest_words` function: ```python from typing import List from game import Game from cats import time def fastest_words(game: Game) -> List[List[str]]: num_players = len(game.times) num_words = len(game.words) fastest = [[] for _ in range(num_players)] for w in range(num_words): fastest_time = float('inf') fastest_players = [] for p in range(num_players): t = time(game, p, w) if t < fastest_time: fastest_time = t fastest_players = [p] elif t == fastest_time: fastest_players.append(p) for p in fastest_players: fastest[p].append(game.word_at(w)) return fastest ``` This function takes in a `Game` object as input, and returns a list of lists of words indicating which words each player typed fastest. The `Game` object has two attributes: `words` and `times`. `words` is a list of strings representing the words in the game. `times` is a list of lists representing the time it took each player to type each word. To compute the fastest words, we iterate over each word in the game. For each word, we determine the fastest time among all players, as well as which players achieved that time. We then append the word to the list of fastest words for each of those players. Note that in the case of a tie, we consider the earliest player in the list to be the one who typed it the fastest. This is done by always appending the word to the list of the first player in the `fastest_players` list. Finally, we return the `fastest` list, which contains one list of words for each player indicating which words they typed fastest.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值