算法复习笔记 | 排序算法比较
2013-09-14 00:32
507 查看
最近正好复习复习算法,于是从排序算法开始做一个总结。以下的代码均为原创,如果有任何问题,欢迎指正。简单来讲,排序算法的实质是将长度为n的数组中的数字按照从小到大或者从大到小的顺利排列。
简而言之,在不考虑算法的情况下,我们可以把排序抽象为如下的一个函数:array表示T类型的一个数组,num表示数组的长度。本文假设我们实现的排序算法都是按照从小到大的顺序排列;从大到小的排列类似。
template <class T>
void Order(T array[], int num)
1)选择算法:基本思想是每次从数组中选择最小的数,将这个数与已排序的数组最后一位交换。寻找第一个数时,我们需要遍历num个数才能判断出最小的数;寻找第i个数时,由于我们已经成功的将前i-1个数按序排列,我们只需要遍历num-i+1个数便能找到最小的数。
总而言之,时间复杂度为O(n2),而空间复杂度为O(1)。
2)简单插入算法:基本思想如同整理扑克牌,一开始手中有1张牌,每抽一张牌便将新抽的牌插入到手中有序的牌列种。
总而言之,时间复杂度为O(n2),而空间复杂度为O(1)。
3)折半插入算法:基本思想和简单插入算法相同,但是新牌与手牌比较的时候不必一一比较(因为手牌已经有序排列了),我们可以用二分查找的方法来将新牌左移
总而言之,时间复杂度有所提高,为O(nlogn),而空间复杂度为O(1)。
4)希尔排序:基本思想和简单插入算法类似。简单插入算法每一次从距离为一的位置处抽取新值。如果数据分布比较分散,简单插入算法效率较低。于是,我们可以采用距离(称之为步长)大于一的方式来抽取新值。例如,当步长为n时,数组中相当于有n个平行的子数组在做简单插入算法。运算之后,数组变得更加为有序,于是我们可以不断地缩小步长直到步长为1(简单插入算法),最终完成排序。当数组越有序时,简单插入算法的效率越高。
总而言之,时间复杂度不容易计算,但实验统计结果大约为n1.25到1.6n1.25,而空间复杂度为O(1)。希尔是一种不稳定的算法。
5)归并排序:初始时,我们假设我们得到了num个长度为1的子数组;每一次运算时将两个有序的子数组合并成一个父数组。
总而言之,时间复杂度为O(n2),而空间复杂度为O(n)。
6)冒泡排序:通过依次交换两个相邻的两个数,遍历num个数后便将最大的数推送到了最后。推送第一个数时,我们需要遍历num个数才能推出最大值;推送第i个数时,由于我们已经成功的将i-1个数按序推出,我们只需要遍历num-i+1个数便能推出当前最大的数。
总而言之,时间复杂度为O(n2),而空间复杂度为O(1)。
7)快速排序:对冒泡排序法的优化,基本思想也是将两个数交换,但是我们希望尽量将较小的数交换到数组左边,将交大的数交换到数组右边。计算时我们从数组中随机选择一个数作为参考数,定义一个指针从左向右遍历,另一个指针从右向左遍历。第一个指针寻找大于参考数的数,第二个指针寻找小于参考数的数,并将两个数交换。当两个指针相遇时一次循环结束,循环结束时相遇位置的左侧的数均小于参考数,右侧的数均大于参考数。我们可以递归地对左侧和右侧的数组运用快速排序算法。
总而言之,时间复杂度为O(nlogn),而空间复杂度为O(1)。
8)堆排序:堆就是一颗树,分为最小化堆与最大化堆,最小化堆的特点是父节点大于等于子节点。堆的存储可以利用数字完成,位置为0的节点表示更节点。对于父节点n,左右子节点为n*2+1与n*2+2。
对于一个父节点两个子节点的子堆,我们可以分成方便的转化使父节点大于等于子节点。整个堆的整理便是循环递归计算。
总而言之,建堆时间复杂度为O(n),删除、插入等操作时间复杂度为)(logn)。而空间复杂度为O(n)。
简而言之,在不考虑算法的情况下,我们可以把排序抽象为如下的一个函数:array表示T类型的一个数组,num表示数组的长度。本文假设我们实现的排序算法都是按照从小到大的顺序排列;从大到小的排列类似。
template <class T>
void Order(T array[], int num)
1)选择算法:基本思想是每次从数组中选择最小的数,将这个数与已排序的数组最后一位交换。寻找第一个数时,我们需要遍历num个数才能判断出最小的数;寻找第i个数时,由于我们已经成功的将前i-1个数按序排列,我们只需要遍历num-i+1个数便能找到最小的数。
总而言之,时间复杂度为O(n2),而空间复杂度为O(1)。
template <class T> void selectSort(T array[], int num) { T curItem;//哨兵元素,用于记录遍历时的较小值,将所以为排序数遍历后就是最小值 int cur;//记录较小值或最小值的位置 for(int i = 0; i < num; i++)//i代表当前有多少个元素已经成功排序 { curItem = array[i]; cur = i; //寻找未排序数的第一个数 for(int j = i + 1; j < num; j++) { if(curItem >= array[j]) { curItem = array[j]; cur = j; } } //从未排序数种找出最小值 if(cur != i) { T temp = array[cur]; array[cur] = array[i]; array[i] = temp; } //如果最小值不是未排序数种的第一个,则交换最小值与未排序数的第一个。 } }
2)简单插入算法:基本思想如同整理扑克牌,一开始手中有1张牌,每抽一张牌便将新抽的牌插入到手中有序的牌列种。
总而言之,时间复杂度为O(n2),而空间复杂度为O(1)。
template <class T> void simpleInsertSort(T array[], int num) { for(int i = 1; i < num; i++)//初始时有一张牌,每抽取一张是一次循环 { int cur = i;//当前新抽取的牌 for(int j = i - 1; j >= 0; j--) { if(array[j] > array[cur]) { T temp = array[j]; array[j] = array[cur]; array[cur] = temp; cur = j; } else { break; } } //手中的牌是有序的,于是每抽一张就一次往前比较。新牌比前面的牌小便往前移动直到手牌整理完成 } }
3)折半插入算法:基本思想和简单插入算法相同,但是新牌与手牌比较的时候不必一一比较(因为手牌已经有序排列了),我们可以用二分查找的方法来将新牌左移
总而言之,时间复杂度有所提高,为O(nlogn),而空间复杂度为O(1)。
template <class T> void binaryInsertSort(T array[], int num) { for(int i = 1; i < num; i++)//初始时有一张牌,每抽取一张是一次循环 { int low = 0; int high = i - 1; //用两个位置坐标来表示手牌 while(low <= high) { int middle = low + (high - low) / 2; if(array[middle] <= array[i]) { low = middle + 1; } else { high = middle - 1; } }; //利用二分法找到插入的位置 if(low != i) { T temp = array[i]; for(int j = i; j > low; j--) { array[j] = array[j-1]; } array[low] = temp; } //如果插入的位置与新牌的位置不同,则将新牌插入 } }
4)希尔排序:基本思想和简单插入算法类似。简单插入算法每一次从距离为一的位置处抽取新值。如果数据分布比较分散,简单插入算法效率较低。于是,我们可以采用距离(称之为步长)大于一的方式来抽取新值。例如,当步长为n时,数组中相当于有n个平行的子数组在做简单插入算法。运算之后,数组变得更加为有序,于是我们可以不断地缩小步长直到步长为1(简单插入算法),最终完成排序。当数组越有序时,简单插入算法的效率越高。
总而言之,时间复杂度不容易计算,但实验统计结果大约为n1.25到1.6n1.25,而空间复杂度为O(1)。希尔是一种不稳定的算法。
template <class T> void shellSort(T array[], int num) { int multipe = 3;//循环次数 int step = 1; for(; step < num; step = step * multipe + 1); step = (step - 1) / 3; //计算出该数组支持的最长步长 while(step >= 1) { for(int i = 0; i < step; i++)//给定步长时平行计算的子数组数目 { for(int j = i + step; j < num; j += step) { int cur = j; for(int k = j - step; k >= i; k--) { if(array[k] > array[cur]) { T temp = array[cur]; array[cur] = array[k]; array[k] = temp; cur = k; } } } //步长为step的简单插入算法 } step = (step - 1) / 3;//缩短步长 } }
5)归并排序:初始时,我们假设我们得到了num个长度为1的子数组;每一次运算时将两个有序的子数组合并成一个父数组。
总而言之,时间复杂度为O(n2),而空间复杂度为O(n)。
template <class T> T minValue(T a, T b) { return a<b?a:b; } //两个值中取较小值 template <class T> void merge(T from[], T to[], int low, int high, int length)//从数组from中合并从low到high之间的子数组并保存到数组to中:第一个数组长度为length,第二个数组长度可能为length或小于length { int first = low;//第一个子数组的开始位置 int second = low + length;//第二个子数组的开始位置 int cur = low;//目标存储数组中的开始位置 while(first < low + length && second <= high) { to[cur++] = minValue<T>(from[first],from[second]); from[first]<from[second]?first++:second++; };//依次选择两个子数组中较小值添加到目标存储数组中 while(first < low + length) { to[cur++] = from[first++]; };//若第一个子数组还有未添加的值,添加到目标存储数组的尾部 while(second <= high) { to[cur++] = from[second++]; };//若第二个子数组还有未添加的值,添加到目标存储数组的尾部 } //归并排序 template <class T> void mergeSort(T array[], int num) { if(num <= 0) return; int len = 1;//子数组长度 int cur = 0;//初始化指针 T* tempArray = new T[num];//临时存储数组 while(len<num)//子数组长度小于num时循环,大于num时排序结束 { while(cur<num) { int m = minValue<int>(cur + len * 2, num); //两个子数组的总长度,考虑越界 for(int i = cur; i < m; i++) { tempArray[i] = array[i]; } //将数值临时存储在临时存储数组中 merge(tempArray, array, cur, m - 1, len); //合并相邻子数组 cur += len * 2; //指针指向下一组子数组 }; //依次选择相邻的子数组合并 len *= 2; cur = 0; //长度翻倍且重置指针 }; delete tempArray; }
6)冒泡排序:通过依次交换两个相邻的两个数,遍历num个数后便将最大的数推送到了最后。推送第一个数时,我们需要遍历num个数才能推出最大值;推送第i个数时,由于我们已经成功的将i-1个数按序推出,我们只需要遍历num-i+1个数便能推出当前最大的数。
总而言之,时间复杂度为O(n2),而空间复杂度为O(1)。
template <class T> void bubbleSort(T array[], int num) { for(int i = 0; i < num; i++) { for(int j = 0; j < num - i; j++) { if(array[j-1] > array[j]) { T temp = array[j-1]; array[j-1] = array[j]; array[j] = temp; } } //依次交换相邻的数将最大值推送到数组尾部已排序的数组最前端 } }
7)快速排序:对冒泡排序法的优化,基本思想也是将两个数交换,但是我们希望尽量将较小的数交换到数组左边,将交大的数交换到数组右边。计算时我们从数组中随机选择一个数作为参考数,定义一个指针从左向右遍历,另一个指针从右向左遍历。第一个指针寻找大于参考数的数,第二个指针寻找小于参考数的数,并将两个数交换。当两个指针相遇时一次循环结束,循环结束时相遇位置的左侧的数均小于参考数,右侧的数均大于参考数。我们可以递归地对左侧和右侧的数组运用快速排序算法。
总而言之,时间复杂度为O(nlogn),而空间复杂度为O(1)。
template <class T> void quick(T array[], int low, int high) { if(high - low > 10) { simpleInsertSort(array + low, high - low + 1); return; } //长度小于10时采用简单插入算法 T key = array[low];//利用第一个数作为参考数 int i = low; int j = high; while(true) { for(;array[i] <= key && i < j;i++);//从左向右选择大于参考数的数 for(;array[j] > key && j > i;j--);//从右向左选择小于等于参考数的数 if(i == j) { break; } //两个指针相遇时这次循环结束 T temp = array[i]; array[i] = array[j]; array[j] = temp; //交换两个指针的数 }; if(i == j) { if(array[i] > key) { quick<T>(array, low, i-1); quick<T>(array, i, high); } else { quick<T>(array, low, i); quick<T>(array, i + 1, high); } } //针对左侧右侧循环采用快速循环 } //快速排序 template <class T> void quickSort(T array[], int num) { int low = 0; int high = num - 1; quick<T>(array, low, high); }
8)堆排序:堆就是一颗树,分为最小化堆与最大化堆,最小化堆的特点是父节点大于等于子节点。堆的存储可以利用数字完成,位置为0的节点表示更节点。对于父节点n,左右子节点为n*2+1与n*2+2。
对于一个父节点两个子节点的子堆,我们可以分成方便的转化使父节点大于等于子节点。整个堆的整理便是循环递归计算。
总而言之,建堆时间复杂度为O(n),删除、插入等操作时间复杂度为)(logn)。而空间复杂度为O(n)。
template <class T> class Heap { public: Heap(T array[], int num); ~Heap(); T pop();//取出根节点 void insert(T value);//插入新节点 private: void buildHeap();//建堆 void switchHeap(int n);//计算一个父节点两个子节点的子堆 T* data;//数据空间 int len;//已有数据长度 int maxLen;//最大可以容纳数据长度 }; template <class T> Heap<T>::Heap(T array[], int num) { maxLen = num * 2; data = new T[maxLen]; len = num; //定义数据,记录已有长度,并预留相同的空间用于堆增长 for(int i = 0; i < num; i++) { data[i] = array[i]; } //初始化已有数据 for(int i = num; i < num * 2; i++) { data[i] = array[i]; } //初始化预留数据 buildHeap(); //建堆 } template <class T> Heap<T>::~Heap() { delete data; data = null; len = 0; } template <class T> void Heap<T>::buildHeap() { int level = floor(log(len)/log(2)) + 1; //根据长度计算树的高度,高度为l的树共2的l次方-1个数据 for(int i = pow(2, level) - 2; i >= 0; i--) { switchHeap(i); } //从树倒数第二层,叶节点的父节点向根节点循环,置换每个三节点子堆 } template <class T> void Heap<T>::switchHeap(int n) { if(n * 2 + 2 < len)//包含左右节点时 { int m = data[n * 2 + 1]<data[n * 2 + 2]?n * 2 + 1:n * 2 + 2; //找到左右节点中较小值 if(data >data[m]) { T temp = data ; data = data[m]; data[m] = temp; } //如果较小值比父节点小,交换 switchHeap(n * 2 + 1); switchHeap(n * 2 + 2); //置换子节点中的子堆 } else if(n * 2 + 1 < len)//只包含左节点 { if(data >data[n * 2 + 1]) { T temp = data ; data = data[n * 2 + 1]; data[n * 2 + 1] = temp; } //如果子节点比父节点小,交换 switchHeap(n * 2 + 1); //置换子节点中的子堆 } //没有子节点不计算 } template <class T> T Heap<T>::pop() { T temp = data[0]; //取出根节点 data[0] = data[len - 1]; len--; //将最后一个数放到根节点 switchHeap(0); //整理根节点 return temp; } template <class T> void Heap<T>::insert(T value) { if(len == maxLen)//如果长度超过最长可用长度,扩展空间 { maxLen = maxLen * 2; T* tmp = new T[maxLen]; //分配新空间 for(int i = 0;i<len;i++) { tmp[i] = data[i]; } //复制数据 for(int i = len;i<maxLen;i++) { tmp[i] = data[i]; } //初始化预留数据 delete data; data = tmp; tmp = NULL; } data[len++] = value; //将插入值放入数组末尾 for(int i = len - 1;i>=0;i--) { if(data[(i - 2) / 2]>data[i]) { T temp = data[(i - 2) / 2]; data[(i - 2) / 2] = data[i]; data[i] = temp; } else { break; } } //沿着树枝将最小值向根节点推送 }
相关文章推荐
- [算法学习笔记]几个排序算法的比较
- java 常用算法--复习笔记--基本排序算法
- [笔记]算法复习笔记---排序算法(快速排序)
- [笔记]算法复习笔记---排序算法(桶排序、冒泡排序)
- 经典算法(8)- 插入排序(Insertion Sort) 及三个基本排序算法的比较
- 白话经典算法-常见排序算法的实现与性能比较
- 基本算法复习之排序:性能比较、代码分析
- 算法复习笔记(分治法、动态规划、贪心算法)
- 基本算法自学笔记(1)八大排序算法
- 代码笔记:排序算法初级比较
- 算法复习笔记-BST
- 数据结构与算法复习笔记
- 【复习笔记】听说找工作一定要会的三种排序算法
- 算法学习笔记:排序算法整理
- 【斯坦福---机器学习】复习笔记之生成学习算法
- 算法复习笔记-绪论
- 算法—比较两种排序算法:选择排序和插入排序(详细)
- 用HTML5实现的各种排序算法的动画比较 及算法小结
- 数据结构与算法复习笔记
- [笔记]算法复习笔记---数组、集合、散列表(下)