排序(上)—-插排、希尔、堆排
插入排序
给每个新增的数在已有的数列中找到合适的位置,然后插入进去
例如一个由小到大的排序:
原数组为:4, 5, 2, 9, 3, 6, 8, 7
- 现在我们有了一个数组,我们可以假装现在排过序的只有第一个数,也就是说已有序列是第一个数(i和j是下标,key代表当前要插入的数,j代表当前已经插入到了第几个数,i是动态的用来与key作比较)
- 开始插入第二个数:从插入数的前一个位置(当前插入arr[1],所以i从0开始)向前寻找第一个不小于它的数,并进行交换,因为我们要把要插入的数插入到第一个(从插入的位置向前数)小于等于它的数的后面,所以当我们遇到比key大的数的时候,就直接将这个数向后挪动即可。
- 第一次当走完0时,没有发现比要插入的数小的,所以下标为j这个数插入完毕,进行查入下一个数,即j=2,同时i从j的前一个开始进行比较
- 我们发现i所在的数比要插入的数大,所以直接进行挪动,但是由于i没有走完0的下标,所以还要继续向前走(因为挪动后arr[j]的值就会变为5,但是我们现在时插入2,所一直要用2来进行比较,因此使用一个key来记住我们当前要插入的数)。
- 当i走到数组的尽头,或者遇到小于等于它的数据时说明要插入的数据就可以插入到其下一个下标的位置。
- 每个数的插入方式都是这样,并且每插入一个数,已经插入的数就已经有序,所以当插完整个数组的时候,整个整个数组的排序也就完成了。
- 因为我们使用的是当遇到的数小于等于要插入的数的进行插入,所以会相对稳定,如果只是小于的话,那么碰到等于的数的时候,还会再进行向后挪动一次,这样,时间不但增加,而且稳定性还会大大降低。
插入排序的时间复杂度:
- 最快:O(n)
- 最慢:O(n^2)但是很稳定
空间复杂度:O(1)
以下是代码:
//插入排序
void InsertSort(int arr[], int size)
{
int i, j;
int key;
for (j=1; j<size; j++)
{
key = arr[j];
for (i=j-1; i>=0; i--)
{
if (arr[i] > key )//如果大于要插入的数,说明要插入的数肯定再此数之前,所以直接将此数向后挪动即可
{
arr[i + 1] = arr[i];
}
else
{
break;
}
}
arr[i + 1] = key;//运行到此位置说明已经找到了要插入的位置
}
}
插入排序的优化
因为我们每次插入的时候,前面的数就已经有序,而我们使用的是遍历式查找比较,所以我们可以使用二分查找来增加查找的速度,当然使用二分查找适合使用数据比较多的时候。
- 在使用二分查找的时候注意二分查找插入位置的条件
以下是代码:
void BInsertSort(int arr[], int size)
{
int i, j;
int key;
for (j = 1; j<size; j++)
{
key = arr[j];
//首先是找要插入的下标
int left, right, mid;
left = 0;
right = j-1;
while (left <= right)
{
mid = left + ((right - left) >> 2);
if (key >= arr[mid])//要插入的位置再mid的右边
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}
//要插入的位置已经找到
//先将此位置到已插入的数的位置的所有数据都向后移动
for (i = j-1; i > mid; i--)
{
arr[i + 1] = arr[i];
}
//此时已经将mid后面的数全部向后挪动了一遍,所以只需要将key插入到当前的位置
arr[i] = key;
}
}
希尔排序
当待排列的数是逆序的时候,插入排序就需要O(n)的时间复杂度,为了减少坏情况时排序的时间复杂度,我们使用多次插入排序,前几次排序时为了让数据基本有序,然后最后一次插入排序的时间复杂度就基本为O(n)了。
希尔排序就是对插入排序的优化,可以认为希尔排序是多次插入排序
- 插入排序的时候,我们一个一个数挨着的插入,但是,希尔排序的前期是排序相隔相等的数,进行排序。例如数据:
- 从图中我们可以知道,如果直接用来插入排序的话,排到后面的时候,时间复杂度会非常大所以我们首先将所有的白色进行排列的得到如下,因为我们是间隔三个数据进行排列的,所以速度是相当快的:
- 排完白色,再将其他两个颜色也都分别排一下:
- 最后将所有的数据进行一次插入排序,而此时数据已经基本有序了,所以最后一次插入排序的时间复杂度基本为O(n)。
- 时间复杂度
- 最好O(n)
- 最坏O(1.3n)
一下是代码:
void ShellSort(int arr[], int size)
{
int i, j, gap;
int key;
gap = 3;//这里取间隔为3
int g = size / gap;//g为根据数据的个数来确定前期插排的次数
while (g > 0)
{
for ( j=2*g-1; j<size; j+= gap)
{
key = arr[j];
for (i=j-gap; i>=0; i-=gap)
{
if (arr[i] > key)
{
arr[i + gap] = arr[i];
}
else
{
break;
}
}
arr[i + gap] = key;
}
g--;
}
//此时输出的好是:4 0 1 7 5 2 9 6 8,符合前期的期望,而再次进行插排时间复杂度就会大大减少
InsertSort(arr, size);
}
上面的方式代码看起来应该很清楚思路,但是总是觉得循环层数优点多,因为我们使用的是一层一层的排序,相对于上面的图来说就是一个颜色一个颜色的进行排序,但是如果我们能直接从第一个开始,每进入一个颜色,都能进行一次该颜色的排序,那么我们就不需要四一层循环了,我们可以通过简单的修改代码,来实现。
void ShellSort(int arr[], int size)
{
int i, j, gap;
int key;
gap = 3;//这里取间隔为3
for ( j=1; j<size; j++)
{
key = arr[j];
for (i=j-gap; i>=0; i-=gap)
{
if (arr[i] > key)
{
arr[i + gap] = arr[i];
}
else
{
break;
}
}
arr[i + gap] = key;
}
//此时输出的好是:4 0 1 7 5 2 9 6 8,符合前期的期望,而再次进行插排时间复杂度就会大大减少
InsertSort(arr, size);
}
堆排序
因为堆的性质,顶部的数总是最大的或者最小的,所以,我们可以通过这个性质用堆来排序。
例如由大到小排序:
将所给数据建立一个小堆,堆顶数据总是最小的。
选堆顶的数据和堆的最后一个数据进行交换,然后进行向下调整,调整的时候不要调整已经交换的最大的数据,也就是不认为交换过后,这个最大的数还在堆里。
- 继续选出堆顶和最后一个数据进行交换,此时最后一个数据不包括之前交换过的数据,也就是说,每次和堆顶元素的交换,认为堆的元素个数就会少一个(从堆尾少)。
- 直到堆的元素为0,算排序完毕,此时整个数据就是有序的了,因为每次交换都是把最大的像后面搬,并且是从后到前的顺序是。
- 时间复杂度都是O(n*logn)
流程图:
代码:
//向下调整
void AdjustDown(int arr[], int parent, int size)
{
//判断是不是已经是叶子结点了
while ( parent*2+1 < size )
{
//默认最小的孩子为左孩子,因为右孩子可能为空
int minChild = 2 * parent + 1;
//存在右孩子,并且右孩子更小
if (minChild+1 < size && arr[minChild+1] < arr[minChild])
{
minChild = minChild + 1;
}
if (arr[parent] > arr[minChild])//父结点比最小的孩子大,就进行交换
{
//先进行交换
int tmp = arr[parent];
arr[parent] = arr[minChild];
arr[minChild] = tmp;
//再让父节点继续向下走
parent = minChild;
}
else//符合堆了
{
return;
}
}
}
//堆排序
void HeapSort(int arr[], int size)
{
//建堆
for (int i=(size-2)/2; i>=0; i--)
{
AdjustDown(arr, i, size);
}
//建好了
//让堆顶的数据和堆中最后一个数据进行交换,并且每交换一次,堆的大小减少一个
for (int i=size-1; i>=0; i--)
{
int tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
//交换完成之后进行向下调整
AdjustDown(arr, 0, i);
}
}