[原创] 基础中的基础(一):简单排序算法总结(附代码)
又到了一年毕业生找工作的时候,回想起自己找工作的经历,那种紧张、期盼、失望和纠结的情景仍然历历在目。在这里跟大家分享一些作者在找工作时总结的基础知识,希望能够大家带来一点点帮助^_^。作者菜鸟一枚,即使总结这些简单的基础知识,也难免出错,还请各位不吝赐教!
本文主要总结了一些很基础的排序算法。通过一张表格,将这些算法的部分要点总结在一起,以方便查找和对比。本文最后给出了一些排序算法的C语言实现供大家参考。本文并不对各个算法进行详细介绍,有需要的读者请自行查找(维基百科对每种算法讲的很详细,见本文参考资料)。
本文中的代码在:https://github.com/icemoon1987/sort_algorithm
一、简单排序算法总结
表中简述部分的用语非常随意,很不专业。原因在于我并不赞成机械地背诵算法步骤,而是要理解算法处理方式和逻辑。此处的文字是在笔者理解算法的基础上,对于其中处理要点的口语化叙述,请大家参考。
表中注意事项部分,是根据我在实现算法的过程中出现的错误总结的,同样非常个人化,很可能不全面(或是多余),仅供参考。
序号 |
名称 |
简述 |
复杂度 |
特性 |
注意事项 |
改进 |
1 |
冒泡排序 |
从数组最后开始,两两比较元素(n-1) 如果顺序不对,则交换元素位置 一轮过后,冒一个泡到当前首元素位置 减小数组长度,进入下一轮(n) |
最好:O(n2) 平均:O(n2) 最差:O(n2) 空间:O(1) |
稳定 就地排序 比较排序 |
冒泡方向与比较方向相配合 冒泡停止条件 |
如果前一轮没有交换过数据,则立刻停止 局部冒泡排序 鸡尾酒排序(双向冒泡) 奇偶排序(a[j],a[j+1]一轮j为奇数,一轮为偶数,在多处理器条件下,实现很快) 梳排序(定义gap,每次减1) |
2 |
选择排序 |
每轮找到最小值(n-1) 与本轮首元素交换 减小数组长度进入下一轮(n) |
最好:O(n2) 平均:O(n2) 最差:O(n2) 空间:O(1) |
不一定稳定 就地排序 比较排序 |
每轮的首元素位置 |
堆排序 |
3 |
插入排序 |
从后边拿出一个元素 与前边各个元素比较,如果不符合要求,则依次向后移动前边的元素(n) 符合要求后,放下这个元素 加长数组长度,进入下一轮(n) |
最好:O(n) 平均:O(n2) 最差:O(n2) 空间:O(1) |
稳定 就地排序 比较排序 |
插入元素的位置 插入元素下标越界 |
二分查找排序,通过二分查找,更快找到插入位置 希尔排序,每次比较前进更多 |
4 |
希尔排序 |
规定增量,下标为增量倍数的元素为一组 各组进行插入排序 选择更小的增量,进入下一轮,直到为1 |
最好:O(n) 平均:与增量有关 最差:O(nlogn2) 空间:O(1)
|
不稳定 就地排序 比较排序 |
下标加入增量后的越界 插入元素位置 |
增量序列选择 1、4、10、23…… |
5 |
合并排序 |
将当前数组分为左右两部分 递归,分别排序左右两部分 合并左右两部分,比较两堆中最上边的元素,哪个小就放入合并序列 |
最好:Θ(n) 平均:Θ(nlogn) 最差:Θ(nlogn) 空间:Θ(n) |
稳定 非就地排序 比较排序 分治递归 |
动态开辟空间、释放空间 递归终止条件(p<=r) 哨兵元素 函数输入参数能否取到 中间元素加入分组 |
|
6 |
堆排序 |
将数组整理成堆 交换根节点与最末尾元素交换,堆长减1 在根节点重新生成堆 重复,直到堆长为1 |
最好:O(nlogn) 平均:O(nlogn) 最差:Θ(nlogn) 空间:O(1) |
不稳定 就地排序 比较排序 |
左右孩子下标 下标是否超过堆长检查 建树起始下标 Max_heapify函数递归 |
|
7 |
快速排序 |
选择基准元(最后元素) 小于基准元的元素交换到前方 将基准元与小于其的元素的后一个交换,实现大于基准元的元素都在后方 分为两部分,递归调用 |
最好:Θ(n2) 平均:Θ(nlogn) 最差:Θ(nlogn) 空间:O(1) |
不稳定 就地排序 比较排序 分治 |
基准元本身不加入本组 函数输入参数能否取到 小于基准源元素起始下标 基准元交换元素下标 |
随机快速排序:随机选择基准元 三数取中,选取基准元 尾递归:用控制结构进行递归,而不是直接调用两次函数 |
8 |
计数排序 |
假设元素都小于等于k(或找到最大值) 开辟数组,记录各个元素出现的次数 从最前边,两两相加,计算小于等于元素的个数 从后向前遍历输入数组(保证稳定性),通过以上信息,直接将元素放入输出数组 更新小于等于元素的个数 |
最好:O(n+k) 平均:O(n+k) 最差:O(n+k) 空间:O(n+k) |
稳定 非就地排序 非比较排序 线性时间 |
小于等于元素个数为1的数,放在第一个,下标为0 动态开辟数组大小k+1 下标越界 对输入有要求 |
|
9 |
基数排序 |
一位一位的进行排序(k) 从低位向高位 子排序算法需要稳定(例如计数排序) |
最好:O(kn) 平均:O(kn) 最差:O(kn) 空间:O(kn)
|
稳定 非就地排序 非比较排序 线性时间 |
分割数字算法 |
|
10 |
桶排序 |
输入为[0,1]上的均匀分布 将[0,1]分为n个桶,将输入序列分到各个桶中 对每个桶进行插入排序 合并各个桶的结果 |
最好: 平均:O(n+k) 最差:O(n2) 空间:O(n*K) |
稳定 非就地排序 非比较排序 线性时间 |
|
|
二、算法实现(C语言)
1. 冒泡排序
1 int bubble_sort(int *input, const int len) 2 { 3 int i,j; 4 int tem; 5 6 if(NULL == input || len < 0) 7 { 8 return -1; 9 } 10 11 for(i=0 ; i<len ; ++i) 12 { 13 for(j = len-1 ; j>i ; --j) 14 { 15 if(input[j]<input[j-1]) 16 { 17 tem = input[j]; 18 input[j] = input[j-1]; 19 input[j-1] = tem; 20 } 21 } 22 } 23 return 0; 24 }
2. 插入排序
1 int insert_sort::sort(int *input, const int size) 2 { 3 int i; 4 5 if(NULL == input || size < 0) 6 { 7 return -1; 8 } 9 10 for(i = 1 ; i < size ; ++i) 11 { 12 int key = input[i]; 13 int j = i-1; 14 while(j>=0 && input[j]>key) 15 { 16 input[j+1] = input[j]; 17 --j; 18 } 19 input[j+1]=key; 20 } 21 22 return 0; 23 }
3. 合并排序
1 // 合并排序:主函数 2 void merge_sort(int *a, const int p, const int r) 3 { 4 if(p<r) 5 { 6 int q = (p+r)/2; 7 sort(a,p,q); 8 sort(a,q+1,r); 9 merge_pp(a,p,q,r); 10 } 11 12 return ; 13 } 14 15 // 合并排序:合并函数 16 void merge_pp(int *a, const int p, const int q, const int r) 17 { 18 vector<int> left; 19 vector<int> right; 20 int i; 21 22 for(i = p ; i <= q ; ++i) 23 { 24 left.push_back(a[i]); 25 } 26 27 for(i = q+1 ; i<=r ; ++i) 28 { 29 right.push_back(a[i]); 30 } 31 32 i = p; 33 34 while( (!left.empty()) && (!right.empty()) ) 35 { 36 if(left.front() < right.front()) 37 { 38 a[i++] = left.front(); 39 left.erase(left.begin()); 40 } 41 else 42 { 43 a[i++] = right.front(); 44 right.erase(right.begin()); 45 } 46 } 47 48 while(!right.empty()) 49 { 50 a[i++] = right.front(); 51 right.erase(right.begin()); 52 } 53 54 while(!left.empty()) 55 { 56 a[i++] = left.front(); 57 left.erase(left.begin()); 58 } 59 60 return; 61 }
4. 快速排序
1 // 快速排序:主函数 2 void sort(int *input, const int p, const int r) 3 { 4 if(p<r) 5 { 6 // 将输入数组分割成两部分 7 int q = partition(input, p, r); 8 9 // 递归调用,分别排序两部分 10 sort(input, p, q-1); 11 sort(input, q+1, r); 12 } 13 14 return; 15 } 16 17 // 快速排序:分割函数 18 int partition(int *input, const int p, const int r) 19 { 20 int key = input[r]; 21 int i = p-1; 22 int j = p; 23 int tem = 0; 24 25 for(; j < r ; ++j) 26 { 27 if( input[j] < key ) 28 { 29 ++i; 30 tem = input[i]; 31 input[i] = input[j]; 32 input[j] = tem; 33 } 34 } 35 36 tem = input[i+1]; 37 input[i+1] = key; 38 input[r] = tem; 39 40 return i+1; 41 }
5. 堆排序
1 // 最大堆排序:获取左孩子函数 2 int left(const int i) 3 { 4 return (2*i+1); 5 } 6 7 // 最大堆排序:获取右孩子函数 8 int right(const int i) 9 { 10 return (2*i+2); 11 } 12 13 // 最大堆排序:在某一节点重新生成最大堆函数 14 void max_heapify(int *input, const int i, const int heap_len) 15 { 16 int l = left(i); 17 int r = right(i); 18 int largest = i; 19 int tem = 0; 20 21 if( l < heap_len && input[l]>input[largest] ) 22 { 23 largest = l; 24 } 25 26 if( r < heap_len && input[r]>input[largest] ) 27 { 28 largest = r; 29 } 30 31 if( largest != i ) 32 { 33 tem = input[largest]; 34 input[largest] = input[i]; 35 input[i] = tem; 36 max_heapify(input,largest,heap_len); 37 } 38 39 return; 40 } 41 42 // 最大堆排序:构建最大堆函数 43 void build_max_heap(int *input, const int len) 44 { 45 int i = len/2-1; 46 47 for(; i >= 0 ; --i) 48 { 49 max_heapify(input, i , len); 50 } 51 52 return; 53 } 54 55 // 最大堆排序:主函数 56 void maxheap_sort(int *input, const int len) 57 { 58 int tem = 0; 59 int heap_len = len; 60 61 // 将输入数组生成最大堆 62 build_max_heap(input,len); 63 64 while( heap_len > 0) 65 { 66 // 交换根节点与最末尾节点 67 tem = input[0]; 68 input[0] = input[heap_len-1]; 69 input[heap_len-1] = tem; 70 71 // 在根节点重新生成最大堆 72 --heap_len; 73 max_heapify(input, 0, heap_len); 74 } 75 76 return; 77 }
6. 计数排序
1 // 计数排序:主函数 2 void count_sort(int *input, int *output, const int len) 3 { 4 int max = find_max(input, len); 5 int i; 6 7 int *tem = (int *)malloc(sizeof(int) * (max + 1)); 8 for(i = 0; i < max+1 ; ++i) 9 tem[i] = 0; 10 11 // 记录各个元素出现次数 12 for(i = 0 ; i < len ; ++i) 13 tem[input[i]]++; 14 15 // 对记录数据进行修改:从最前边,两两相加,计算小于等于某个元素的元素个数 16 for(i = 1 ; i < max+1 ; ++i) 17 tem[i] += tem[i-1]; 18 19 // 将元素直接放入输出数组 20 for(i = 0 ; i < len ; ++i) 21 { 22 output[ tem[input[i]] - 1 ] = input[i]; 23 tem[input[i]]--; 24 } 25 return; 26 } 27 28 // 计数排序:寻找最大值函数 29 int find_max(int *input, const int len) 30 { 31 int max = 0; 32 int i; 33 34 for(i = 0 ; i < len ; ++i) 35 if( input[i]>max ) 36 max = input[i]; 37 38 return max; 39 }
好了,本文的主要内容就这些了,最后再跟大家分享几个学习算法的个人心得:
1、学习算法不光是学习知识,更是学习一种思维方式。在积累了一定算法基础之后,相信您对新问题、新算法都能够有自己的感觉和思路。
2、不要背诵算法步骤,要理解算法步骤后的方法和逻辑。单纯地背诵算法没有用处,一定要理解算法解决问题的方法。如果理解了方法,算法步骤不过是用一种有逻辑的方式将这种方法表达出来而已。理解算法对记忆算法也有极大帮助。
3、算法学习和其他计算机知识学习一样,一定要注重实践。在实现的过程中,我们将发现很多算法中的具体问题,对算法的细节和思想都有可能有新的发现。
本文参考资料:
http://en.wikipedia.org/wiki/Sorting_algorithm 维基百科上的那些算法演示 gif 挺有趣的:)
《算法导论》(第二版)