1.直接选择排序、
基本思想:在一个无序的序列中找到其中最大或者最小的元素,将它放在序列首位或者举例末尾,然后从排除首位元素组成的序列重复上述过程,让元素剩余的元素直到为1,结束
具体实现
1.在元素索引从0到n-1中的序列中找到其中最大值或者最小值。
2.如果该元素不是序列首位元素或者末尾元素,将它与首位或者末尾交换。
3.重复上述过程直到元素过程为1
我们定义两个变量begin。和end,让它们初始化为0和n-1,作为数组的索引,我们要程序重复寻找最大值或最小值,这就需要遍历范围为begin,end之间的数组,然后还要重复寻找最大值和最小值,这里就需要两个循环进行嵌套,当数组元素为1的时候,是begin和end相等的时候,此时我们只需要让外层循环的条件为begin<end即可。
void SelectSort(int* arr, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i <= end; i++)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
if (begin == maxi)
{
maxi = mini;
}
Swap(&arr[begin], &arr[mini]);
Swap(&arr[end], &arr[maxi]);
begin++;
end--;
}
}
具体代码实现如上。
在这里需要提及一个特殊的点,当元素的最大值刚好为begin的时候,我们需要特殊处理,我们,我们拿9,6,3这个数组举例,当遍历完找到最大值和最小值的时候,我们找到最大值和最小值9和3的下标为0和2,我们如果不进行特殊处理的话,直接让最小值和起始位置begin进行交换,此时数组变为3,6,9.最大值的下标仍然指向0,然后最大值和最后一个位置交换,数组又变回了9,6,3,此时数组就无法达到一个排序的需求。我们让最大值是begin的时候,让最大值的索引等于最小值的索引,那么此时再完成交换的时候,最小值的索引是2,然后end的索引也是2,让他们进行交换,此时的数组就是3,6,9,此时这个问题就解决了。
时间复杂度
直接排序的思想易于理解,但是它的时间复杂度和冒泡,最坏情况的插入排序一样,为O(n²),因为有内外分别循环n次的循环,时间复杂度过高,效率低下。
2.堆排序
堆排序有两个版本,一个是根据堆的数据结构来进行,另一个是根据堆的数据结构思想来进行实现的。
第一种版本(借助数据结构来实现)
此版本需要我们提前实现好,堆的数据结构的常见方法,且易于理解,只提供思想和参考代码。
首先我们需要将一个无序的数组中的所有元素将他们进行入队。
入完之后,循环取堆顶元素将他放到原数组中,直到堆中的元素为空。
void HeapSort(int* a, int n)
{
HP hp;
for(int i = 0; i < n; i++)
{
HPPush(&hp,a[i]);
}
int i = 0;
while (!HPEmpty(&hp))
{
a[i++] = HPTop(&hp);
HPPop(&hp);
}
HPDestroy(&hp);
}
第二种版本(借助堆的思想来实现)
基本思想
我们首先拿到的是一个无序的序列,我们借助堆的思想,先将这个序列调整为堆,调整完之后,定义一个从序列最后一个位置的索引开始的变量,首位交换,重新调整该序列为堆。让索引自减重复上述过程,直到索引小于0.
在这里分为两个部分
调整序列为堆。
进行排序
我们先介绍第一个
向上调整算法
堆是一种完全二叉树,它的向上调整算法也是依托于二叉树来实现,它的实现需要两个参数,一个指向一片连续的 存储空间的指针,以及有效的元素个数-1(最后一个元素下标)。
说的通俗易懂一点,向上调整算法就是已知孩子找父母。
它有一个公式(孩子坐标-1)/2=父母坐标。该公式适用左右孩子。如果为右孩子,拿4举例,它的父母坐标为1,4-1/2=1,取整之后仍然是1.
找到父母坐标之后和孩子坐标的元素进行比较,按照建造大堆或者小堆需求,进行交换,重复上述过程直到孩子坐标为1,因为坐标为0的地方是根结点,循环根节点就代表结束。
如果其中有一个地方没有进行交换,我们就进行退出。根据二叉树的特性,在往上找就找不到我们满足我们需求的元素了。
void adjustup(int* arr, int child)
{
int parents = (child - 1) / 2;
while (child > 0)
{
if (arr[parents] < arr[child])
{
Swap(&arr[parents], &arr[child]);
child = parents;
parents = (child - 1) / 2;
}
else
{
break;
}
}
}
向下调整算法
该方法有三个参数,指向一片连续空间的指针,指向起始位置的指针,还有有效的元素个数。
在这里有一点需要提及,向下调整算法的本质是已知父母,找孩子。可是对于左右孩子来说,我们并不知道,哪一个是相对大或者相对小(根据建堆需求)。关于已知父节点找子结点的公式是
父节点坐标*2+1=左节点,+2等于右节点。
在进行孩子结点的比较时,要求右孩子的坐标要小于有效元素个数,因为最后一个元素的坐标是n-1,如果只有左节点没有右节点,那么左结点的坐标就是n-1,右坐标就越界了,也就不进行该判断。
该循环结束的条件是孩子结点小于n,当递归结束结束的时候也就来到了最后一个结点,
跟向上调整算法一样,一旦停止交换就跳出循环,表示当前二叉树,再往下走就没有合适的元素了。
代码如下
void adjustdown(int* arr, int parents, int n)
{
int child = parents * 2 + 1;
while (child < n)
{
//找出较大的孩子结点
if (child+1<n&&arr[child] < arr[child + 1])
{
child++;
}
//建造大堆《
if (arr[parents] < arr[child])
{
Swap(&arr[parents], &arr[child]);
parents = child;
child = parents * 2 + 1;
}
else
{
break;
}
}
}
堆排序(向上调整算法版本)
在我们拿到一个无序的序列时,我们要首先将他调整为一个有序的堆,向上调整算法,要从第一个元素调整到最后一个元素,这样才能保证它时有效的堆结构,如果从最后一个元素开始调整,调整一次,只能保证根元素下的两个元素是,满足堆的结构的,所以要从第一个开始。
当对堆进行调整之后,要进行排序
堆排序的两个版本是一样的排序算法,我们首先拿首元素和最后一个元素进行交换,然后删除最后一个元素的索引,对剩下的元素进行向下调整,然后让索引减一
这里有一个非常精妙的i点,我们定义一个索引的变量,最后一个元素的索引是n-1,而这个值也正是有效个数减1.
void heapsort2(int* arr, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
adjustup(arr, i);
}
int end = n - 1;
while (end)
{
Swap(&arr[0], &arr[end]);
adjustdown(arr, 0, end);
end--;
}
}
堆排序(向下调整算法版本)
向下调整算法与向上版本不同的是它是需要从最后一个子结点的父节点从下往上遍历,直到索引小于0,来保证整个堆为有序的。对于最后一个子节点的父节点来说,我们向下调整算法是由3个参数构成的,它给了有效元素个数,让有效元素个数减1就是最后一个元素的索引,根据公式,
(子节点坐标-1)/2=父节点坐标
代码如下
void heapsort(int* arr, int n)
{
int i = 0;
for (i = (n - 2) / 2; i >= 0; i--)
{
adjustdown(arr, i, n);
}
int end = n - 1;
while (end)
{
Swap(&arr[0], &arr[end]);
adjustdown(arr, 0, end);
end--;
}
}