前言
排序可划分为很多类比如比较排序和非比较排序,比较排序又分为内排序和外排序,内排序又非为插入排序(直接插入排序和希尔排序)、选择排序(简单选择排序和堆排序)、交换排序(冒泡排序和快速排序),外排序在这里只介绍一种就是归并排序。以上介绍的都是比较排序,还有一种排序就是不用比较也能排序的非比较排序(计数排序和基数排序)。这篇博客呢我主要介绍比较排序(因为非比较排序不常用),一种排序多种排法,尽可能展现传统排序方法的优化解法(哈哈哈~有点夸张了)
插入排序
直接插入排序
基本思想:
当插入第i(i<=1)个元素时,前面的a[0],a[1]….a[i-1]已经排好序,此时将a[i]与a[i-1],a[i-2]…的顺序进行比较,找到插入位置即将a[i]插入,原来位置上的元素按顺序后移。
代码片
void InsertSort(int* a, size_t n)
{
assert(a);
//i表示要插的数
for (int i = 1; i < n; i++)
{
int tmp = a[i],j=i;
//将j插入到以j-1为结束的有序区间
for ( j = i; j >0; j--)
{
if (tmp < a[j-1])
{
a[j] = a[j-1];
}
else
{
break;
}
}
a[j] = tmp;
}
}
性能分析
- 元素集合越接近有序,直接插入排序算法的时间效率越高(时间复杂度越低)
- 最优情况下时间复杂度为O(n)
- 最差情况下时间复杂度为O(n^2)
- 空间复杂度为O(1),它是一种稳定的排序算法
排序演示
希尔排序
基本思路
先预排序,再直接排序
代码片
void ShellSort(int* a, size_t n)
{
size_t gap = n;
assert(a);
while (gap>1)
{
gap = gap / 3+1;
for (int j = gap; j < n; j = j + gap)
{
int tmp = a[j],k=j;
for (k = j; k>0; k = k - gap)
{
if (a[k-gap]>tmp)
{
a[k] = a[k - gap];
}
else
{
break;
}
}
a[k] = tmp;
}
}
}
性能分析
- 缩小增量排序,是对直接插入排序的优化
- 时间复杂度为O(n^1.3)
- 空间复杂度为O(1),它是一种稳定的排序算法
排序演示
选择排序
直接选择排序
基本思路
在元素集合a[0]~a[n]中选择最大值和最小值,与这组数据的最后一个和第一个元素进行交换
在剩余的a[i]~a[n-i] (i=1,2…)集合中,重复上述步骤,直到集合剩余一个元素
代码片
//直接选择排序(优化版,一次选两个,选出最大值和最小值)
void sawp(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* a, size_t n)
{
size_t begin = 0, end = n - 1;
while (begin < end)
{
size_t k = begin + 1,max=begin,min=begin;
while (k <= end)
{
if (a[min] > a[k])
{
min = k;
}
if (a[max] < a[k])
{
max = k;
}
k++;
}
//最大的数在begin的位置,最小的数在end的位置,这个时候如果交换两次的话就会回到原来的位置(相当于没有交换)
if ((min == end)&&(max==begin))
{
sawp(&a[min], &a[begin]);
continue;
}
sawp(&a[min], &a[begin]);
sawp(&a[max], &a[end]);
begin++;
end--;
}
}
性能分析
- 时间复杂度为O(n^2)
- 空间复杂度为O(1),它是一种不稳定的排序算法
3.它的效率没有插入排序的效率高,因为插入排序最优的情况下时间复杂度是O(n),而选择排序时间复杂度都是O(n^2),并且比较次数多
排序演示
堆排序
基本思路
代码片
堆排序,首先要建一个堆(排升序建大堆,排降序建小堆)这里以排升序为例,所以先建一个大堆
建好堆之后,把堆顶元素a[0]和当前最堆的最后一个元素交换
堆元素个数减一
由于进行完第一步之后根节点不再满足最堆定义,所以要向下调整根节点
直到当前堆剩一个元素停止
void sawp(int* c, int* b)
{
int tmp = *c;
*c = *b;
*b = tmp;
}
void AdjustBwon(int* a, size_t n,size_t m)
{
size_t parent = m, child = 2 * parent + 1;
while (child < n)
{
//防止数组越界,并且找到左右孩子结点中的最大值
if (child + 1 < n&&a[child] < a[child + 1])
{
child++;
}
if (a[parent] < a[child])
{
sawp(&a[parent], &a[child]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, size_t n)
{
assert(a);
//建堆(大堆)
for (int i = n / 2; i >= 0; i--)
{
//向下调整
AdjustBwon(a,n, i);
}
//排序
for (int i = n-1; i>0; i--)
{
sawp(&a[i], &a[0]);
AdjustBwon(a, i, 0);
}
}
性能分析
- 把一颗二叉树调整为堆,以及每次将堆顶元素交换后进行调整的时间复杂度为O(logn),所以堆排序的时间复杂度为O(nlogn)
- 空间复杂度为O(1),它是一种不稳定的排序算法
排序演示
交换排序
冒泡排序
基本思路
冒泡思想很简单(升序为例),就是在集合a[0]~a[n-1] (i=1,2…)中相邻的两个元素两两比较大的放在后面,小的放在前面知道集合中剩一个数为止
代码片
void sawp(int* c, int* b)
{
int tmp = *c;
*c = *b;
*b = tmp;
}
void BubbleSort(int* a, size_t n)
{
assert(a);
for (int i = n-1; i>0; i--)
{
//哨兵,优化作用,一旦排好顺序,就会跳出循环
int flag = 0;
for (int j = 0; j < i; j++)
{
if (a[j]>a[j + 1])
{
flag = 1;
sawp(&a[j], &a[j + 1]);
}
}
if (flag == 0)
{
break;
}
}
}
性能分析
- 最好情况时间复杂度
- 最坏情况时间复杂度
排列演示
快速排序
第一种方法:hoare版本(左右指针法)
基本思路
下面图例是选择最右边的值为key但是写的代码是优化过的(三数取中法),因为我们选的key值是影响快排的关键,如果只是单一的选最右边或者最左边或者最中间的值难免可能会遇到取得key的值全是最小或者最大值,此时快排效率是O(N),为了避免这种情况我们在每次选key值得时候拿区间内的最左值、最右值、最中间的值进行比较,取中间值做key值。
代码片
//找中间数
int GetMidIndex(int*a, int left, int right)
{
int mid = left + ((right - left) >> 1);
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else
{
if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else
{
if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
}
//三数取中法(左右指针法快排的优化)
int QuickSortPart1(int *a, int left, int right)
{
int mid = GetMidIndex(a, left, right);//找在划分的区域内最左、最右、最中间三个数的大小数值上的中间的数
int key, begin, end;
sawp(&a[mid], &a[right]);//中间的数和最右边的数交换
key = a[right];//选最右边的数为key
begin = left, end = right;
while (begin < end)
{
//begin找大
while (begin < end&&a[begin] <= key)
{
begin++;
}
//end找小
while (begin < end&&a[end] >= key)
{
end--;
}
if (begin < end)
{
sawp(&a[begin], &a[end]);
}
}
sawp(&a[begin], &a[right]);
return begin;
}
void QuickSort(int* a, int left,int right)
{
int div;
assert(a);
if (left >= right)
{
return;
}
div = QuickSortPart1(a, left, right);
QuickSort(a, left,div-1);
QuickSort(a, div+1, right);
}
排序演示
第二种方法:挖坑法
基本思路
代码片
int QuickSortPart2(int *a, int _left, int _right)
{
int left = _left, right = _right;
int key = a[_right];
while (left < right)
{
//left找大
while (left < right&&a[left] <= key)
{
left++;
}
a[right] = a[left];
//right找小
while (left < right&&a[right] >= key)
{
right--;
}
a[left] = a[right];
}
a[left] = key;
return left;
}
void QuickSort(int* a, int left,int right)
{
int div;
assert(a);
if (left >= right)
{
return;
}
div = QuickSortPart2(a, left, right);
QuickSort(a, left,div-1);
QuickSort(a, div+1, right);
}
排序演示
第二种方法:前后指针法
基本思路
在这里我们选最右边元素做Key值
有两个下标,一前一后
移动规则是cur先走碰到小于key值的就停止,然后a[cur]和a[++prev]比较如果相等cur就继续往前走,如果不相等就交换两个元素的值
代码片
int QuickSortPart3(int *a, int left, int right)
{
int cur = left, prev = left-1,key;
key = a[right];
while (cur <= right)
{
//如果a[cur]比key大,cur就走prev不动
if (a[cur]> key)
{
cur++;
}
else
{
//如果a[cur]比key小,就比较a[cur]和 a[++prev]是否相等,相等cur就往前走,否则两个值交换
if (a[cur] == a[++prev])
{
cur++;
}
else
{
sawp(&a[cur], &a[prev]);
}
}
}
return prev;
}
void QuickSort(int* a, int left,int right)
{
int div;
assert(a);
if (left >= right)
{
return;
}
div = QuickSortPart3(a, left, right);
QuickSort(a, left,div-1);
QuickSort(a, div+1, right);
}
性能分析
快排可算得上是这几种排序中的较好的排序算法了,时间复杂度O(nlogn),不稳定
排序演示
归并排序
基本思路
将带排序的元素序列划分成两个长度相等的子序列,对每个子序列排序,然后将他们合并成一个序列。
代码片(递归)
void Merge(int* a,size_t left,size_t right)
{
int mid=(right-left)>>1;
int begin1, end1, begin2, end2,i=0;
int *tmp = (int*)malloc(sizeof(int)* 100);
memset(tmp, 0, sizeof(a[0])*100);
assert(a&&tmp);
if (left >= right)
{
return;
}
//划分
Merge(a,left,left+mid);
Merge(a, left + mid+1, right);
//归并
begin1 = left, end1 = mid+left;
begin2 =left+ mid + 1, end2 = right;
while ((begin1 <= end1)&&(begin2 <= end2))
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1];
begin1++;
}
else
{
tmp[i++] = a[begin2];
begin2++;
}
}
while (begin1 <=end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
for (i = left; i <= right; i++)
{
a[i] = tmp[i-left];
}
free(tmp);
}
代码片(非递归)
//二路归并
void MergeR(int* a,int begin,int mid,int end)
{
int i=0;
int *tmp = (int*)malloc(sizeof(int)* 100);
int begin1 = begin, end1 =mid;
int begin2 =mid + 1, end2 = end;
memset(tmp, 0, sizeof(a[0])*100);
assert(a&&tmp);
while ((begin1 <= end1) && (begin2 <= end2))
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1];
begin1++;
}
else
{
tmp[i++] = a[begin2];
begin2++;
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
for (int k = begin; k <= end; k++)
{
a[k] = tmp[k - begin];
}
free(tmp);
}
//一趟二路归并
void MergePass(int*a,size_t n,int length)
{
int i = 0;
assert(a);
//归并时两个区间的划分
for (i = 0; i+2*length-1 < n; i += length * 2)
{
MergeR(a, i,i+length-1,i+2*length-1);
}
//这一步主要是解决当长度很大时,不足以构成俩个完整的归并区间时(此时就只能划分为两个区间),就把两个不完整的区间合并
if (i + length - 1 <n - 1)
{
MergeR(a, i, i + length - 1, n-1);
}
}
//归并
void MergeSortR(int* a, int n)
{
assert(a);
//每次划分的长度length=1,2,4,6,8.....
for (int length = 1; length < n; length *= 2)
{
MergePass(a, n, length);
printf("%d->", length);
Print(a, n);
}
}
性能分析
时间复杂度为O(nlogn),稳定的排序算法。
排列演示
以非递归为例
不足的地方,请多指教~