1.排序的概念及常见的排序算法
排序在咱们日常生活中十分的常见,就好比是网上购物的时候通常能够选择按照什么排序,比如价格、评论数量、销量等。那么接下来咱们就来了解一些关于排序的概念。
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序。
排序也是有很多算法的,一下是常见的排序算法:
接下来咱们分别来说一说这些个常见的排序。
2.插入排序
2.1直接插入排序
插入排序就像是打扑克牌摸牌时的整理牌的动作,取出一张牌,插在两张牌之间形成升序,以此往复,直至牌被整理好。咱们再来观察一下直接插入排序排升序的动图模拟。
首先,咱们来讨论开始情况,初始时,认为第一个元素已排好,①与②比较,如果②大,认为①②已经排好,接下来以②开始和③比较;如果②小,则取出②,①挪到②的位置,②向后没有数可以比较,②停在0的位置,认为①②排好,接下来以②开始和③比较。
接下来,咱们假设一共有n个元素,设end为已排好的序列的最后一项的位置,提前存好end+1的内容赋值到tmp。end与end+1进行比较:
①如果end+1比end要大,那么视为排好,end+1成为新的end继续与新的end+1进行比较;
②如果end+1比end要小,end的内容赋值到end+1中,这时候的end缺少数据,咱们假象tmp停在上面等待加入,接着end--,将end和tmp进行比较:
①如果tmp大于end,就说明前面的数据全都比tmp小,无需再比较,tmp停下比较,停在end+1的位置(end是--后的end);
②如果tmp小于end,那么end的值赋值在end+1的位置(tmp预备赋值的地方),想象tmp悬在end上方,end--,以此往复,直到end减到了-1,或者tmp碰到了比他小的数为止,最终tmp赋值在了end+1处。
以上就是对于单趟排序的分析,以下是插入排序的具体内容,可见插入排序的时间复杂度是O(N^2).
void InsertSort(int* a, int n)
{
for(int i=0;i<n-1;i++)
{
//初始时,认为第一个数据已经排好
int end = i;
//提前存下end+1的内容
int tmp = a[end + 1];
//end为-1的时候结束
while (end >= 0)
{
//tmp小,还没有找到自己的位置
if (tmp < a[end])
{
//end来到end+1的位置
a[end + 1] = a[end];
end--;
}
else//tmp大,说明已经找到位置了,不用继续遍历,退出循环
{
break;
}
}
//将tmp赋值在end+1的位置
a[end + 1] = tmp;
}
}
2.2希尔排序
希尔排序是以插入排序为基础的一种排序,效率比直接排序更高,时间复杂度大概为O(N^1.3),其思想是:①通过预排序使数据趋于有序;②用插入排序排好。
假设一个整型gap,那么预排序就是从0开始,每隔一个gap取一个数(不越界取),取完后进行一次插入排序,然后在从1开始,每隔一个gap取一个数,继续插入排序,直到取完,也就是把插入排序改成如下,gap的值可以为任意值(但是要小于元素个数)。
gap = 3;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
通过预排序,可以快速将前面较大的数据转移到后面。但仅有一个gap的,对数据的有序化有限,此时,就可以遍历gap的值来增强有序化,那么gap的初值选什么合适呢?gap值选大了就容易漏掉很多数据,取小了效率就低,于是有大佬研究,gap取元素个数的三分之一效率会较高。所以当元素个数为n时,gap=n,gap=n/3.
都到这里了,肯定就有人发现了,当gap=1的时候不就是插入排序吗!那能不能把预排序和插入排序结合起来呢?可以是可以,但是咱们要确保gap的最后取值是1呀,于是对gap有了一点改进:gap=gap/3+1。一眼看上去可能看不出什么苗头,但是只要代值进去就会发现,gap最后都是1.
那么最终的代码如下,
void TestShellSort(int* a, int n)
{
int gap = n;
while(gap>1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
3.选择排序
3.1选择排序
选择排序的思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
以排升序,找小数为例,可以使用假设法找到小数下标,先设0为最小数,和1进行比较,如果0小,那么不变,如果1小,那么下标改为1,遍历一趟之后咱们就能将最小的值放在开头,单层循环结束后只需要重新设最小数就能完成排序,于是可以有以下代码,时间复杂度为O(N).
void SeSort(int* a, int n)
{
int left = 0;
while (left < n)
{
int min = left;
for (int i = left+1; i < n; i++)
{
if (a[min] > a[i])
min = i;
}
Swap(&a[min], &a[left]);
left++;
}
}
此时提出一个问题,能不能同时找到最大值和最小值放到左右两边呢?这当然是可以的,但如果最大值位于最小值将要换到的地方或者是最小值位于最大值将要换到的地方呢?咱们需要注意这点,并在交换的时候注意更新数据。
void SelectSort(int* a, int n)
{
int right = n-1;
int left = 0;
while(right>left)
{
int max = left;
int min = left;
for (int i = left+1; i < n-left; i++)
{
if (a[i] > a[max])
{
max = i;
}
if (a[i] < a[min])
{
min = i;
}
}
Swap(&a[min], &a[left]);
//如果重叠,那么更新数据
if (max == left)
max = min;
Swap(&a[max], &a[right]);
right--;
left++;
}
}
3.2堆排序
堆排序是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据需要注意的是排升序要建大堆排降序建小堆。其时间复杂度为O(nlogn)。
在二叉树部分也有提过,其核心就是向下调整算法建堆,再将根与最后一个元素交换,调整堆的大小,在向下调整成新的堆,循环操作得到排好的数据。
至于向下调整算法建堆可以参见往篇,而排序的过程也只是简单的控制定义域。
void AdjustDwon(int* a, int n, int root)
{
//向下调整,大堆
//root*2+1为孩子节点,如果孩子节点存在则循环继续
while(root * 2 + 1<n)
{
//找到两个孩子中大的那个
int child = root * 2 + 1;
if (child+1<n && a[child] < a[child + 1])
{
child = child + 1;
}
if (child<n && a[child] > a[root])
{
Swap(&a[child], &a[root]);
}
root = child;
}
}
void HeapSort(int* a, int n)
{
//最后一个节点的父节点
int parent = (n - 1 - 1) / 2;
//向下调整建堆
for (int i = parent; i >= 0; i--)
{
AdjustDwon(a, n, i);
}
//选择,挑出大的数据
for (int i = n - 1; i > 0; i--)
{
Swap(&a[0], &a[i]);
AdjustDwon(a, i - 1, 0);
}
}
今天的排序就到这里啦!至于剩下的排序就留到下篇吧!