常用算法总结
最近为了面试,在恶补算法的一些知识,算是尝到了大学没有好好学习苦头,博文是转载的,我为了加深一些映像就自己写一篇,顺便加了一些自己的理解,有错误的话希望各位能够指正。
排序算法大体可分为两种:
一种是比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
另一种是非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。
这里我们来探讨一下常用的比较排序算法,非比较排序算法将在下一篇文章中介绍。下表给出了常见比较排序算法的性能:
排序算法稳定性的定义:
排序算法稳定性的简单形式化定义为:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。
排序算法稳定性的好处:
排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。
冒泡排序
这个算法就不用介绍了吧,基本每个初学者第一个接触的算法就是冒泡算法,名字的由来就是没经过一轮比较,最大或最小的数字就会像气泡一样浮到最尾段。
冒泡算法的工作顺序为:
1、比较相邻的元素,如果需要调换位置的就进行调换
2、对每个元素重复步骤一,到最后一个元素将会是最大的元素
3、返回开始继续重复上述的步骤,除了最后一个元素
4、继续重复上述的步骤,直至没有元素任何一对数字需要比较
// BubbleSort
void swap(int A, i, j) {
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void BubbleSort(int A, int n) {
for(int i = 0; i < n-1; i++) {
for(int j = 0; j < n-i-1; j++) { //每比完一轮,最大元素就浮到最后面,继续比较浮完元素的前面元素
if(A[j] > A[j+1]) {
swap(A, j, j+1);
}
}
}
}
void mian() {
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
int n = sizeof(A) / sizeof(int);
BubbleSort(A, n);
printf("冒泡排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
实现过程如下:
冒泡算法的优点就是易于理解,但是是很没有效率的算法(如果一个数组有n个数,那么排序完成后需要比较n*(n-1)/2次)
选择排序
选择排序也是一种简单直观的排序算法。它的工作原理很容易理解:初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序跟冒泡排序最大区别在于:冒泡排序在遍历的过程中重要符合条件都会进行交换,而选择排序则在遍历过程中记住了当前的最大或最小元素,才进行位置的交换
#include <stdio.h>
void swap(int A[], i, j) {
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void SelectSort(int A[], int n) {
for(int i = 0; i < n-1; i++) {
int min = i;
for(int j = i+1; j < n-1; j++) {
if(A[min] > A[j]) {
min = j;
}
}
if(min != i) {
swap(A, min, i);
}
}
}
void mian() {
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
int n = sizeof(A) / sizeof(int);
SelectSort(A, n);
printf("冒泡排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
选择排序实现过程:
选择排序是不稳定的算法:
比如序列:{ 5, 8, 5, 2, 9 },第一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。
插入排序
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。插入排序的特点就是比较过的元素始终是排好序的。
具体算法描述如下:
1、默认第一个元素是排好序的,从第二个元素开始进行向前比较
2、与上一个元素进行比较,如果比上一个元素小的话,则向前挪一位
3、重复步骤二,直到上一个元素比该元素小
4、将该元素插入到比较元素的后面
5、重复上述步骤,直到最后一个元素
#include <stdio.h>
void InsertionSort(int A[], int n) {
for(int i = 1; i < n; i++) { //默认第一个元素是排好序的
int right = A[i];
int left = i - 1;
while(j >= 0 && right < A[left]) { //当等于的时候不进入循环,因此插入排序是稳定的
A[left] = A[left+1]; //如果右边的元素比左边的要小,就需要换位置
left--;
}
A[left+1] = A[right]; //最后将元素插入到合适的位置中
}
}
void main() {
int A[] = { 6, 5, 3, 1, 8, 7, 2, 4 };// 从小到大插入排序
int n = sizeof(A) / sizeof(int);
InsertionSort(A, n);
printf("插入排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
上述代码对序列{ 6, 5, 3, 1, 8, 7, 2, 4 }进行插入排序的实现过程如下:
插入排序是稳定的算法,在数量级较小的时候速度还是比较快的,但是当数量级多的时候,元素的移动会变得很费时间和效率。插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
二分插入排序
因为插入排序前面比较过的元素始终是排序好的,所以后面的元素在查找插入位置的时候可以用二分查找,可以减少比较次数。
#include <stdio.h>
void InsertionSortDichotomy(int A[], n) {
for(int i = 1; i < n; i++) {
int left = 0;
int right = i-1;
int ins = A[i];
while(left <= right) {
int mid = (left + right) / 2;
if(A[mid] > ins) {
right = mid - 1;
} else {
left = mid + 1;
}
}
for(int j = right; j >= left; j--) {
A[j+1] = a[j];
}
A[left] = ins;
}
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 从小到大二分插入排序
int n = sizeof(A) / sizeof(int);
InsertionSortDichotomy(A, n);
printf("二分插入排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。
快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治策略(Divide and Conquer)来把一个序列分为两个子序列。步骤为:
1、从序列中挑出一个元素,作为"基准"(pivot).
2、把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
3、对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
#include <stdio.h>
void swap(int A[], i, j) {
temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void Partition(int A[], left, right) {
int pivot = A[right]; //将最后一个元素假设为基准值
int tail = left - 1;
for(int i = left; i < right; i++) {
if(A[i] <= pivot) {
swap(A, ++tail, i);
}
}
swap(A, ++tail, right);
return tail;
}
void QuitSort(int A[], left, right) {
if(left >= right) {
return;
}
int pivot = Partition(A, left, right);
QuitSort(A, left, pivot-1);
QuitSort(A, pivit+1, right);
}
int main()
{
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 }; // 从小到大快速排序
int n = sizeof(A) / sizeof(int);
QuickSort(A, 0, n - 1);
printf("快速排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。
转载自: 常用排序算法总结.