@BIGBALLON
2017-10-28T13:32:56.000000Z
字数 10542
阅读 1386
layout: post
title: 排序算法小结
categories: [datastructures]
[algorithm, data structure, sorts]班上一位同学要搞博客,,,来问我。。
我就打开了很久没管过的博客,,发现数学公式貌似比以前好那么点了。
遂发一个markdown上来看看效果,说实话效果并不算太好,当然,还能看的。
就是这样!!!喵喵!
另外。。。。。。标题等属性,,yao 置顶 a。。。。。
前几天有同学找我要排序算法的代码,说实话我已经有几百万年没写过排序了。
电脑里找了一下发现代码都不见了,留下的也都是丑得一B的代码,不忍直视。。
冒泡,选择这样的东西自从开始用之后就几百年没用过了。
另外这个博客仅仅用于我自己整理我自己的排序算法代码。
至于找工作什么的要强行加上 函数返回值 和 。。就自己就加上吧。
现在也就这水平了,只能写写排序了,、 什么的早就写不出来了。。。
如果有巨巨看到我的这篇博文别喷我。毕竟半退役ACM弱渣。。。
另外本文写于 。。。。。目前没有准备更新的意思。。
直接插入排序,属于最基本的插入排序。
基本思想就如同打扑克抓牌:
- 时间复杂度:
很容易看出插入操作需要进行 次。
对于最好情况,也就是序列本身有序,如我们抓牌的顺序是
我们共比较了 次,且没有交换次数,故时间复杂度为
对于最坏情况,也就是序列本身逆序,如我们抓牌的顺序是
此时比较次数为
交换次数为
所以总的时间复杂度约为- 空间复杂度:
就不多废话了- 稳定性:
直接插入排序是稳定的。逐个元素依次插入,相等元素的前后顺序是不会改变的,所以显然是稳定的。
void insert_sort( int arr[], int sz ){int i , j , val;for( i = 1; i < sz; ++i ){val = arr[ i ];j = i - 1;while( j >= 0 && arr[j] > val ){arr[ j + 1 ] = arr[ j ];--j;}arr[ j + 1 ] = val;}}
很明显可以看到,,直接插入排序慢的要死(在一般情况下)。
故有人对它进行优化。。(其实并没有什么卵用)
直接插入排序中我们在确定插入位置的时候是逐个数进行比较。
这里就进行了一点优化,用二分进行快速定位
(如果你还不会写二分那你可以去撞墙了)。
- 时间复杂度:
无奈在元素的交换还是耗费了大量时间,平均的时间复杂度显然还是
事实上,因为二分的缘故,,,最优情况退化为- 空间复杂度:
,也不多废话了- 稳定性:
直接插入排序是稳定的,,那显然折半插入也是稳定的,,不要问我为什么。。
void b_insert_sort( int arr[], int sz ){int i, j, val, low, high, mid;for( i = 1; i < sz; ++i ){low = 0, high = i - 1;val = arr[ i ];while( low <= high ){mid = ( low + high ) >> 1;if( val > arr[ mid ] ){low = mid + 1;}else {high = mid - 1;}}for( j = i; j > low; --j ){arr[ j ] = arr[ j - 1 ];}arr[ low ] = val;}}
我引用网络上好多博客都写的一句话:
2-路插入排序是在折半插入排序的基础上再改进之
好一个在折半插入排序的基础上再改进。。也不知道是那些博主抄来抄去还是怎么回事。
写个烂代码根本就没有折半插入,,,还强行说自己在写“2-路插入排序”,文不对题也是NB,更猛的是下面还一群人评论,写得好,,,哪里写得好了?明显误人子弟。。
要么就写清楚自己的代码是基于折半插入排序的2路插入排序还是基于直接插入排序的2路插入排序。
强行加一句文不对题的话是真的理解还是???
看了这篇 博客 写得好,起码还有点复杂度分析,,其他那些代码是不是抄来的都不知道,复杂度分析估计博主压根不会。
2-路插入排序需要一个辅助数组,并且这个数组是一个环状数组(循环数组)
- 时间复杂度:
虽然用了二分可以将比较的时间降为
但是我们计算数据交换的次数为 ,所以时间复杂度还是
具体计算,有 的数据不需要交换位置。
剩下 的数据每次移动交换
如果从最坏情况考虑。。还是会退化到- 空间复杂度:
,需要有一个辅助数组用来存放数据- 稳定性:
稳定的。- 关于取模:
有人说这个算法的精髓在于取模,,我说如果取模都没有这个算法还搞个P,其实取模操作的耗时也是很长的,,这点在搞数论的时候深有体会。
//基于折半插入排序的2路插入排序void tp_insert_sort( int arr[], int sz ){int* d = new int[ sz ];d[ 0 ] = arr[ 0 ];int final, first, val, i, j;final = first = val = 0;for( i = 1; i < sz; ++i ){val = arr[i];if( val < d[ first ] ){first = ( first - 1 + sz ) % sz;d[ first ] = val;} else if( val > d[ final ] ){final = ( final + 1 + sz ) % sz;d[ final ] = val;} else{int low = 0, high = ( final - first + sz ) % sz;int end = high;while( low <= high ){int mid = ( low + high ) >> 1;if( val < d[ ( mid + first ) % sz ] ){high = mid - 1;}else{low = mid + 1;}}for( j = end; j >= high + 1; --j ){d[ ( j + first + 1 ) % sz ] = d[ ( j + first ) % sz ];}d[ ( high + first + 1 ) % sz ] = val;final = ( final + 1 + sz ) % sz;}}for( i = 0; i < sz; ++i ){arr[i] = d[ ( i + first ) % sz ];}delete [] d;}//直接插入排序的2路插入排序void t_insert_sort( int arr[], int sz ){int* d = new int[ sz ];d[ 0 ] = arr[ 0 ];int final, first, val, i, j;final = first = val = 0;for( i = 1; i < sz; ++i ){val = arr[i];if( val < d[ first ] ){first = ( first - 1 + sz ) % sz;d[ first ] = val;} else if( val > d[ final ] ){final = ( final + 1 + sz ) % sz;d[ final ] = val;} else{int pos = ( final + 1 + sz ) % sz;while( val < d[ ( pos - 1 + sz ) % sz ] ){d[ ( pos + sz ) % sz ] = d[ ( pos - 1 + sz) % sz ];pos = ( pos - 1 + sz ) % sz;}d[ ( pos + sz ) % sz ] = val;final = ( final + 1 + sz ) % sz;}}for( i = 0; i < sz; ++i ){arr[i] = d[ ( i + first ) % sz ];}delete [] d;}
这东西没什么用,,就不说了。其实是我不会。。。
#define SIZE 100#define MAX 0x7ffffffftypedef struct{int val;int next;}SLNode,*PSLNode;typedef struct{SLNode num[ SIZE ];int len;}SLinkList, *PSLinkList;int init_link( PSLinkList Slist, int arr[], int sz ){if( sz >= SIZE ){return 0;}Slist->num[0].val = MAX;Slist->num[0].next = 1;for( int i = 1; i <= sz; ++i ){Slist->num[i].val = arr[ i - 1 ];Slist->num[i].next = 0;}Slist->len = sz + 1;return 1;}int table_sort( PSLinkList Slist ){int len = Slist->len;for( int i = 2; i < len; ++i ){int idx = 0;int pos = Slist->num[0].next;while( Slist->num[i].val > Slist->num[pos].val ){idx = pos;pos = Slist->num[pos].next;}Slist->num[i].next = pos;Slist->num[idx].next = i;}return 1;}int arrange( PSLinkList Slist ){int i, idx, pos;pos = Slist->num[0].next;for( i = 1; i < Slist->len; ++i ){while( pos < i ){pos = Slist->num[pos].next;}idx = Slist->num[pos].next;if( pos != i ){std::swap( Slist->num[pos], Slist->num[i] );Slist->num[i].next = pos;}pos = idx;}}
其实shell排序很简单,就是将原数组分成若干个子序列,
对每个子序列分别使用直接插入排序。
最后再对全体元素进行一次直接插入排序。
实际上这里就是有一个叫做 步长 的东西。
- 时间复杂度:
时间是和所取“增量”序列的函数密切相关,通常取的步长可以是原数组的一半,
然后不断减小直到1为止。
平均情况下为 ---- 注意这里我选择的步长是不断除以2
其实我不会计算 的时间复杂度,原谅我,我太笨了- 空间复杂度:
- 稳定性:
不稳定。为什么不稳定?可以这样考虑,因为对数组进行了“分组”,存在相等元素位置发生改变的情况。
void shell_sort( int arr[], int sz ){int i, j, d, val;d = sz >> 1;while( d >= 1 ){for( i = d; i < sz; ++i ){j = i - d;val = arr[i];while( j >= 0 && arr[j] > val ){arr[ j + d ] = arr[ j ];j -= d;}if( j != i - d ){arr[ j + d ] = val;}}d >>= 1;}}
- 时间复杂度:
这个东西真的很慢,最近一次写这个排序估计要追溯到大一了。。
算法总共会进行 次扫描,每次扫描会进行 次比较
那么时间复杂度自然是- 空间复杂度:
- 稳定性:
不稳定,可能导致相等元素相对位置发生改变。
void select_sort( int arr[], int sz ){int i, j, pos;for( i = 0; i < sz - 1; ++i ){pos = i;for( j = i + 1; j < sz; ++j ){if( arr[pos] > arr[j] ){pos = j;}}if( pos != i ) {std::swap( arr[pos], arr[i] );}}}
这玩意又叫做“锦标赛排序”,确实很像,就是淘汰赛咯。
我们可以将数组看成是一颗满二叉树的叶子节点(不能够成满二叉树就补全)。
具体算法执行步骤我简要写一下吧,如果看不懂不要打我就是了。
- 建树,用数组实现的话,树根为0号单元,建树的时候从尾往前建树
- 取得最小值,然后将最小值对应的叶子节点标记为MAXX,向上更新直到树根,得到次小值
- 取得次小值,然后将最小值对应的叶子节点标记为MAXX,向上更新直到树根,得到次次小值
- 如此反复知道排序完毕
如果你不知道我在说什么我找了一个 PPT
- 时间复杂度:
由数组构造树需要
选择最小关键字耗时,以后每次选择一个次小关键字需要消耗
总的时间复杂度 =- 空间复杂度:
辅助空间比较多,且需要进行对此多余的比较- 稳定性:
它是稳定的,如果不能理解可以举一些特例,最后会发现它并不会改变相等元素的相对位置。
#define MAXX 0x7fffffff; //int maxvoid tree_select_sort( int arr[], int sz ){int len, i, j, idx, cnt, minn;len = ( sz << 1 ) - 1;int* tree = new int[ len ];//精髓所在,由后往前填充,且后面的处理也是从后往前,并且是-2//如果是构造数据结构的话,需要进行节点补充。for( i = sz - 1, j = 0; i >= 0; --i, ++j ){tree[ len - 1 - j ] = arr[ i ];}for( i = len - 1; i > 0; i -= 2 ){int pos = ( i - 1 ) >> 1;if( tree[ i - 1 ] < tree[ i ] ){tree[ pos ] = tree[ i - 1 ];}else{tree[ pos ] = tree[ i ];}}cnt = 0;while( cnt < sz ){minn = tree[ 0 ];arr[ cnt++ ] = minn;idx = len - 1;while( tree[ idx ] != minn ){--idx;}tree[ idx ] = MAXX;while( idx > 0 ){if( idx & 1 ){tree[ idx >> 1 ] = tree[ idx ] < tree[ idx + 1 ] ? tree[ idx ] : tree[ idx + 1 ];idx = idx >> 1;}else{tree[ ( idx - 1 ) >> 1 ] = tree[ idx - 1 ] < tree[ idx ] ? tree[ idx - 1 ] : tree[ idx ];idx = ( idx - 1 ) >> 1;}}}delete [] tree;}
这里的堆值的是二叉堆,他其实是遵循一定规律的数组。
最大堆:所有节点的子节点比其自身小的堆。
最小堆:所有节点的子节点比其自身大的堆。
- 时间复杂度:
由数组构建堆的时间复杂度其实并不是
更精确的应该是线性的
排序过程中,每次重新调整最大堆需要的时间,总共有个元素,需要调整次,耗时
总耗时
具体的证明看 《算法导论》 或者 这个 blog- 空间复杂度:
- 稳定性:
不稳定
void max_heapify( int arr[], int i, int sz ){int lt = ( i << 1 ) + 1, rt = ( i << 1 ) + 2;int lar = 0;if( lt < sz && arr[ lt ] > arr[ i ] ){lar = lt;}else{lar = i;}if( rt < sz && arr[ rt ] > arr[ lar ] ){lar = rt;}if( lar != i ){std::swap( arr[ i ], arr[ lar ] );max_heapify( arr, lar, sz );}}void build_maxheap( int arr[], int sz ){for( int i = sz >> 1; i >= 0; --i ){max_heapify( arr, i, sz );}}void heap_sort( int arr[], int sz ){build_maxheap( arr, sz );int len = sz - 1;while( len >= 1 ){std::swap( arr[ 0 ], arr[ len ] );--len;max_heapify( arr, 0, len );}}
冒泡排序没什么说的,基本没用。。
- 时间复杂度:
最原始的冒泡排序的时间复杂度是,最好情况也是
改进之后(加入了flag标记),最优情况可以优化为
总之,平局情况算法的时间复杂度为,一般情况也不会用冒泡排序。。。- 空间复杂度:
- 稳定性:
临位交换必然是稳定排序。
void bubble_sort2( int arr[], int sz ){int i, j, flag;for( i = 0; i < sz; ++i ){flag = 0;for( j = 1; j < sz - i; ++j ){if( arr[ j - 1 ] > arr[ j ] ){std::swap( arr[ j ], arr[ j - 1 ] );flag = 1;}}if( flag == 0 ){break;}}}void bubble_sort( int arr[], int sz ){for( int i = 0; i < sz - 1; ++i ){for( int j = sz - 1; j > i; --j ){if( arr[ j - 1 ] > arr[ j] ){std::swap( arr[ j ], arr[ j - 1 ] );}}}}
快速排序是对冒泡排序的一种改进,运用的是递归的思想
具体步骤如下:
1. 定基准:从数列中跳出一个元素,作为基准"pivot"(这个基准其实还是很重要的,可直接规定,也可以使用随机化)
2. 分区操作:对数列进行重新排序,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
3. 自顶向下递归调用
- 时间复杂度:
我只记得原来看算法导论的时候书上写了好几页,用来分析时间复杂度的,,,简单理解的话就是,总共会有次递归,每次进行 的比较,大概来说是
最坏情况下时间复杂度会退化为,但是在一般情况下,快速排序是相当优秀的,不然怎么能叫做快速排序呢?
详细严谨的证明最理想的就是回去翻算法导论了。我就不BB了- 空间复杂度:
平均情况需要的栈空间,
最坏情况下会退化到的栈空间,- 稳定性:
不稳定
我原来学快排的时候在想为什么叫做霍尔快排。。
当然这是废话,因为快排这个鬼东西是霍尔发明的。
霍尔快排取数列中的第一个数为基准,
从尾向头扫描,遇到比基准小的数则将其和low位置交换(尽量让它往左边交换)
从尾向头扫描,遇到比基准大的数则将其和high位置交换(尽量让它往右边交换)
最后将基准放到中间去,这样就完成了将比基准小的数放在它左边,将比基准的的数放在它右边的操作了。
int partition( int arr[], int lt, int rt ){int low = lt, high = rt;int val = arr[ lt ];while( low < high ){while( low < high && arr[ high ] >= val ){--high;}arr[ low ] = arr[ high ];while( low < high && arr[ low ] <= val ){++low;}arr[ high ] = arr[ low ];}arr[ low ] = val;return low;}void quick_sort( int arr[], int lt, int rt ){if( lt < rt ){int pivot = partition( arr, lt, rt );quick_sort( arr, lt, pivot - 1 );quick_sort( arr, pivot + 1, rt );}}
我原来一开始用的是这个快排。
当然现在变成XX了,都不会写了。
int partition( int arr[], int lt, int rt ){int val = arr[ rt ];int i = lt - 1, j;for( j = lt; j <= rt - 1; ++j ){if( arr[j] <= val ){++i;std::swap( arr[ i ], arr[j] );}}std::swap( arr[ i + 1 ], arr[ rt ] );return i + 1;}void quick_sort( int arr[], int lt, int rt ){if( lt < rt ){int pivot = partition( arr, lt, rt );quick_sort( arr, lt, pivot - 1 );quick_sort( arr, pivot + 1, rt );}}
随机化其实就是加了一个随即选择基准,代码什么的基本一样的。
最初这个写法是在上海交大的某个模板上学来的。
当时觉得好短好强悍!而且这个代码很好理解!!
void quick_sort( int arr[], int beg, int end ){int i = beg, j = end, x= arr[ ( beg + end ) / 2 ];do{while( arr[ i ] < x ) ++i;while( arr[ j ] > x ) --j;if( i <= j ) std::swap( arr[ i++ ], arr[ j-- ] );}while( i < j );if( i < end ) quick_sort( arr, i, end );if( j > beg ) quick_sort( arr, beg, j );}
快排的优化相当多,,很多我不会。。就不误人子弟乱写了。
好像有什么三平均分区法,并行快排什么的,,不懂。
这种归并排序是用递归实现的,也是我们最常写的归并排序。
但是显然递归会增加额外的开销,但是迭代能省下这些开销而且迭代的效率要高一些。
递归过程,将待排序的数组一分为二,
将子数组一分为二。
将子数组一分为二。。
直到待排序的子数组只剩下一个元素为止,然后不断合并两个子数组。
- 时间复杂度:
最原始的归并排序的时间复杂度是- 空间复杂度:
简单理解为
实际上归并排序的空间复杂度应该是,所以还是- 稳定性:
必须稳定啊。
void merge_arr( int arr[], int lt, int mi, int rt, int d[] ){int i = lt, j = mi + 1, k = 0;;while( i <= mi && j <= rt ){if( arr[i] <= arr[j] ){d[ k++ ] = arr[ i++ ];}else{d[ k++ ] = arr[ j++ ];}}while( i <= mi ){d[ k++ ] = arr[ i++ ];}while( j <= rt ){d[ k++ ] = arr[ j++ ];}for( i = 0; i < k; ++i ){arr[ lt + i ] = d[ i ];}}void merge( int arr[], int lt, int rt, int d[] ){if( lt < rt ){int mid = ( lt + rt ) >> 1;merge( arr, lt, mid, d );merge( arr, mid + 1, rt, d );merge_arr( arr, lt, mid, rt, d );}}int merge_sort( int arr[], int sz ){int *d = new int[ sz ];if( d == NULL ){return 0;}merge( arr, 0, sz - 1, d );delete []d;return 1;}
这种排序实际上就把递归改为迭代。
但是明显迭代是优越的。
这个迭代我在大三之前只写过一次。。惭愧。
迭代的思想:
将数组中相邻元素两两配对,将它们变为有序。
这样会构成 组长度为 的已排序子数组段。
然后再将相邻的长度为2的子数组段两两配对,
排序之后变为 组长度为 的已排序子数组段。
如此反复(其实就是不断增大步长)
直到整个数组有序即可。
原数组(排序前):112 2 44 33 13 44 42 0 0 444 555 235253 0 228 92 300 8 235 2
第一次(步长1) :2 112 33 44 13 44 0 42 0 444 555 235253 0 228 92 300 8 235 2
第二次(步长2) :2 33 44 112 0 13 42 44 0 444 555 235253 0 92 228 300 2 8 235
第二次(步长4) :0 2 13 33 42 44 44 112 0 0 92 228 300 444 555 235253 2 8 235
第二次(步长8) :0 0 0 2 13 33 42 44 44 92 112 228 300 444 555 235253 2 8 235
第二次(步长16):0 0 0 2 2 8 13 33 42 44 44 92 112 228 235 300 444 555 235253
int it_merge_sort( int arr[], int sz ){int i, lt_min, rt_min, lt_max, rt_max, next;int *d = new int[ sz ];if( d == NULL ){return 0;}for( i = 1; i < sz; i *= 2 ){for( lt_min = 0; lt_min < sz - i; lt_min = rt_max ){rt_min = lt_max = lt_min + i;rt_max = lt_max + i;if( rt_max > sz ){rt_max = sz;}next = 0;while( lt_min < lt_max && rt_min < rt_max ){d[ next++ ] = arr[ lt_min ] < arr[ rt_min ] ? arr[ lt_min++ ] : arr[ rt_min++ ];}while( lt_min < lt_max ){arr[ --rt_min ] = arr[ --lt_max ];}while( next > 0 ){arr[ --rt_min ] = d[ --next ];}}}delete []d;}
如果你能看到这里,,真的很感谢你,我写得这么烂的这篇博客我自己都目不忍视了。
另外,想说明几点。
要找资料,推荐去wiki,不要上百度,,百度百科里面的东西都是老掉牙的,几乎都是从wiki搬过来的。
而且的更新比较快,特别是英文版的页面。
的末尾还会提供非常多的论文和参考文献,非常好
所以想详细了解最好去 。
还有就是去阅读一些专业的参考书籍(例如算法导论之类的)。
一般的数据结构书写得太简单,太草率,证明什么的更是完全没有。