排序在我们的日常生活中无处不在,比如对若干个学生的期末成绩,可以依据姓氏,学号,某科成绩,总成绩等进行排名,富豪榜的排名,游戏中的战力排名,甚至是在日常生活中的扑克牌,我们也会依据其数字大小等对其进行排序,所以我们在学习编程的过程中,排序一定是十分重要并且对于我们十分有帮助的东西,所以本篇文章,将对几大重要排序算法–冒泡排序,插入排序,希尔排序,选择排序,堆排序,快速排序,归并排序进行讲解。
冒泡排序,几乎是所有编程初学者接触到的第一种排序算法,虽然由于其时间复杂度过大没有什么实际的应用价值,但其还是有很强的教学意义的。
冒泡排序的基本思路就是相邻的两个元素两两比较,不断地将较大/小的值往后放,一趟下来最后一个元素就是最大/小值,然后再重复此过程即可。
冒泡排序的代码实现如下
void BubbleSort(int arr[], int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 1; j < n - i; j++)
{
if (arr[j - 1] > arr[j])
{
int temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
}
}
}
}
冒泡排序的时间复杂度是O(N^2),可见消耗是十分大的。
选择排序和冒泡排序的思想是类似的。
选择排序和冒泡排序唯一的不同就是,冒泡排序是比较相邻的元素,而选择排序是将一个元素和它后面所有的元素进行比较.也没有什么难度。
选择排序的代码实现如下
void SelectSort(int arr[], int n)
{
for (int i = 0; i < 9; i++)
{
for (int j = i + 1; j < 10; j++)
{
if (arr[i] > arr[j])
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
选择排序的时间复杂度是O(N^2),时间消耗和冒泡一样,也是十分巨大的。
插入排序的思想就和我们平时打扑克牌时理牌的思路差不多
我们平时在打扑克牌时,我们一般是摸一张整理一张,这样可以保证我每次放进一张牌之前的牌都是有序的,然后将我们要插入的牌从后往前(之所以不从前往后,是因为我们插入牌,相当于插入数据的时候,需要将后面的所有数据依次往后挪动,为这个数据(牌)留出一个位置)依次进行比较,放入合适的位置。
插入排序的代码实现如下
void InsertSort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++)
{
// 2 3 4 3
int end = i;
int temp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > temp)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = temp;
}
}
插入排序的算法也是O(N^2),但是它有别于前两个算法,因为数据越接近有序,它的时间效率就越高,不像前两个排序一样就算有序仍然要进行比较。而且它还能作为接下来要讲的一个很强的排序–希尔排序的辅助实现。
希尔排序是对直接插入排序的优化。
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成n/gap个
组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。
当gap>1时叫做预排序,目的是使数组接近有序,因为假设我们考虑一个最坏的情况,数组最开始是降序排列的,我们要将其升序排列,相当于位于第一个位置的最大的数要放到最后的位置去,如果我们用插入排序的思想去排,那么要比较长度次才能将这个数放到最后去,这样消耗是十分大的,但是我们可以增大比较的数据之间的间隔,以此来减小比较的次数,
这样假设有10个数据,如图
希尔排序的代码实现如下
void ShellSort(int arr[], int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int temp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > temp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = temp;
}
}
}
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定。
学习堆排序前我们要首先了解什么是堆。
堆其实就是一种二叉树,堆满足以下两种性质:
1. 堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
要实现堆排序,我们首先要实现一个堆,然后才能进行后续操作,而堆虽然是一个完全二叉树,但在本质上,还是以数组的形式存储的.这里我们要用两种算法–向上调整建堆和向下调整建堆。
向上调整建堆在插入数据以前必须要保证之前的数组满足是一个大堆或者小堆,向上调整建堆,顾名思义,就是要将插入到尾部的数据在其祖先这条线路上进行迭代调整,如图圈出来的部分就是我们要调整的部分。
我们不难发现这是一个小堆,那么此时我们就要将插入的3放到正确的位置上去,我们经过如图的操作即可实现,最终3会到根节点的位置。
向下调整建堆,其实就是调整根节点的位置,但在这之前要保证其左右子树都是大堆或者小堆。
然后我们要比较父亲和左右孩子的大小,如果是小堆,我们就要选出左右孩子种较小的那个,比如图中的10,我们就要和5进行对调,如果和9进行对调,9变成父节点比5大,就不满足小堆了。所以正确的方法是如图所示的路径。
在学习完这两种算法后我们就要来尝试实现堆排序了。
1。首先我们通过向上/向下调整算法建立一个小堆(大堆也行,这里用小堆举例)。建立的思路就是依次往数组里放入数据,最开始只有一个数据的时候我们可以看做是一个堆,以此类推,我们就能满足每次插入数据之前都是一个堆。
2.在创建完堆后,我们后续操作出来的数组是升序还是降序就取决于这是一个大堆还是小堆,我们第一步这说的是建立小堆,那么我们最终得到的数组也只能是降序,反之则是升序,这是为什么呢?
我们建立了小堆,最小的数就在根节点的位置,但由于其物理上是数组,如果我们直接在根节点把这个最小的数取出来,后面的所有数据父子关系就全乱了。所以正确的思路是把其根节点与最后一个数据交换,保证其堆的结构大致不变(即左右子树还是堆),接着我们根据这个特性首先想到向下调整算法,继续选出次小的数,再和倒数第二个数交换,这样以此类推,我们就会发现数组就成为降序排列了。
堆排序的代码实现如下
void AdjustUp(int arr[],int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(int arr[], int n,int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && arr[child + 1] > arr[child])
{
child = child + 1;
}
if (arr[parent] < arr[child])
{
Swap(&arr[parent], &arr[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int arr[], int n)
{
//向上调整建堆
for(int i=1;i<n;i++)
AdjustUp(arr,i);
//升序--建大堆
//降序--建小堆
int end = n - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end--;
}
}
本篇内容到这里就结束了,如有出入,欢迎指正。