关于算法导论中讲到的一些排序算法,还有一些没讲到的,以后会添加,如,希尔排序,选择排序。
- 插入排序[Inserttion Sort]: 对于输入数组A[n],n代表数组大小。算法总保持A[1···j-1] 区间内的元素已排好序,把a[j]插入到这个数组中使得a[1····j]是有序的,当j==n-1时,整个数组有序,排序完成,当已排好序数组元素只有一个,显然是有序的。就好像我们打牌时候摸牌的过程,总保持手上的牌是排好序的,新模上来的牌找到合适的位置插入,保持整个手牌有序,到最后也是有序的。 代码如下:
//The insertion sort asending order void InsertionSortInt( int arr[], int len) { for(int j = 1;j < len; j++ ) { int key = arr[j]; int k = j - 1; while(k >= 0 && arr[k] > key) //keep the elements before arr[j] always were sorted { // find the position that arr[k-1] < key <= arr[k]and insert; arr[k+1] = arr[k]; k--; } arr[k+1] = key; } }
- 归并排序(Merge Sort): 归并排序基于分治法思想,所谓的分治法:1、把问题分解成一系列的子问题;2、递归解决各个子问题。若子问题足够小,直接求解;3、将子问题的解合并成为原问题的解。合并排序思想:1、将n个元素分成含n/2个元素的子序列;2、用合并排序对两个子序列进行排序;3、合并两个已排序的子序列得到排序结果。当子序列长度为1时递归,显然已经是有序,递归结束。 算法分为练个函数,核心在于合并函数,代码如下:
思考题2-1:用插入排序优化合并排序,虽然合并排序的最坏运行时间好于插入排序,但当n比较小时,事实上插入排序要来的更快。所以在合并排序算法递归过程中,当子问题足够小,设为长度为k的子序列,使用插入排序。 2-4、逆序对:思考在合并排序过程中的比较过程,递归计数即可。//MergeSort ascending sort //This is a recursion process void Merge( int arr[], int left, int mid, int right ) { int lNum = mid - left + 1; int rNum = right - mid; int *L = new int[lNum+1](); int *R = new int[rNum+1](); for(int i = 0; i < lNum; i++) { L[i] = arr[left+i]; } for(int i = 0;i < rNum; i++) { R[i] = arr[mid + i +1]; } L[lNum] = R[rNum] = 0x7FFFFFFF; //cout <<0x7FFFFFFF <<endl; //merge int i = 0; int j = 0; for(int k = left; k <= right; k++) { if( L[i] < R[j] ) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } } delete [] L; delete [] R; } void MergeSortInt( int arr[], int left, int right ) { if(left < right) { int mid = (left+right) / 2; MergeSortInt( arr, left, mid ); MergeSortInt( arr, mid+1, right ); Merge( arr, left, mid, right ); } }
- 冒泡排序(bubble Sort):冒泡排序就是美其名为冒泡排序而已,算法本身一点不高效。但也的确简单。伪代码如下:
每次do for循环都使得a[1····i]有序。BufferSort(A) for i = length[A]; do for j = length[A] downto i+1 do if A[j] < A[j-1] then Exchange(A[j],A[j-1])
- 堆排序(Heap Sort ): 堆:一棵完全二叉树,树中的每个父节点总大于或小于孩子节点。 对一个输入数组保持堆的性质(最大堆为例,ps:在调用这个递归过程我们总假设,此节点的左右孩子已经符合最大堆的性质):当一个节点的权值比孩子节点要小不符合堆的性质,找出它与孩子节点中最大的值与之交换,此时这个节点已经符合最大堆的性质,但交换后的孩子节点可能不符合最大堆的性质,递归维持子节点,知道叶子节点。代码如下:
建堆:对一个输入数组建堆。调用上述MaxHeapify()函数,从大到小维护每个个非叶子节点的堆属性,完全二叉树的性质:对于一个元素数量为n的完全二叉树,其叶子节点为:(n/2)+1·····n;void MaxHeapify( int arr[], int i, int heapSize ) { while(true) { int lChild = Left( i ); int rChild = Right( i ); int largest = i; if( lChild < heapSize && arr[lChild] > arr[largest] ) { largest = lChild; } if( rChild < heapSize && arr[rChild] > arr[largest] ) { largest = rChild; } if( largest != i ) { EXCHANGE( arr[largest], arr[i] ); } else break; //当前根节点不符合堆性质,进行了调整,那么新调整可能导致调整后的子树 //不符合堆性质,继续调整 i = largest; } }
实现堆排序:先对输入数组建立最大堆,每次从堆的顶端取出最大的元素与数组最后一个元素交换,然后维护最大堆的,重复取数知道对中袁术为0.void BuildMaxHeap( int arr[], int size ) { int heapSize = size; for( int i = size/2; i >= 0; i-- ) { MaxHeapify( arr, i, heapSize ); } }
完整代码:void HeapSort( int arr[], int size ) { int heapSize = size; BuildMaxHeap( arr, size ); for( int i = size-1;i >= 1;i--) { EXCHANGE( arr[i], arr[0] ); heapSize--; MaxHeapify( arr, 0, heapSize ); } }
练习6.5-7:HeapDelete()的实现,思想:从最大堆中删除一个元素,考虑一般情况,删除的是一个非叶子节点,删除的结果是——无论被删除节点是其父节点的左孩子还是右孩子都必然造成其后面节点的右孩子变为左孩子,兄弟节点的左孩子变成他的右孩子。由于本来是最大堆,现在的情况是,被删除节点之后节点只有右孩子有可能不符合最大堆的性质,因为左孩子是本来节点的右孩子必然符合。所以只需要从非叶子节点开始向前维护右孩子的最大堆性质知道被删除节点的父节点即可。 6.5-8:以每个链表的第一个元素为堆的元素建立堆,每次取堆中的第一个元素(此元素肯定为最大或最小),把对应链表的下一个元素放到堆顶,其实就是指针的移动。维持堆属性,重复取数。取出来的元素的顺序就是合并后链表的顺序。/*********************************************************** * Copyright ?2013 CoderLing * * E-mail:coderling@gmail.com * * blog:http://blog.youkuaiyun.com/coderling * ***********************************************************/ #ifndef HEAP_H #define HEAP_H #include <iostream> #define EXCHANGE( a, b ) (a) = (a)+(b); (b) = (a)-(b); (a) = (a)-(b) int Left( int i ) { return 2*i + 1; } int Right( int i ) { return 2*i + 2; } //---------最大堆----------------// //维持堆属性,这里有个前提,就是其左右子树必须满足堆属性 //特别注意:由于我们的数组下标都是从0开始,所以这里建立 //了以零为根节点的二叉树,对于节点i,左孩子为(2*i+1) 右孩子为(2*i+2); void MaxHeapify( int arr[], int i, int heapSize ) { while(true) { int lChild = Left( i ); int rChild = Right( i ); int largest = i; if( lChild < heapSize && arr[lChild] > arr[largest] ) { largest = lChild; } if( rChild < heapSize && arr[rChild] > arr[largest] ) { largest = rChild; } if( largest != i ) { EXCHANGE( arr[largest], arr[i] ); } else break; //当前根节点不符合堆性质,进行了调整,那么新调整可能导致调整后的子树 //不符合堆性质,继续调整 i = largest; } } void BuildMaxHeap( int arr[], int size ) { int heapSize = size; for( int i = size/2; i >= 0; i-- ) { MaxHeapify( arr, i, heapSize ); } } void HeapSort( int arr[], int size ) { int heapSize = size; BuildMaxHeap( arr, size ); for( int i = size-1;i >= 1;i--) { EXCHANGE( arr[i], arr[0] ); heapSize--; MaxHeapify( arr, 0, heapSize ); } } //0 #endif
- 快排(QuickSort):公认的最实用的排序算法,有着良好的平均运行时间。快排也是基于分治法思想:1、把输入数组A[p,r]分解为A[p,q-1],A[q+1,r]两个数组;2、递归调用快排对两个子数组进行排序;3、因为两个子数组为就地排序,并不需要合并操作,所以整个数组已经有序。快排的核心于划分算法,下面给出完整的快排源代码,为根据书上一些练习优化之后的版本:
由于我在看完书才去实现代码,昨晚后才发现,没对其进行随机化的优化。。。。。 思考题7-3:”漂亮的算法“Stooge排序,T(n) = 3T(2n/3) + c;由主定理得:Θ(n^l0g(3/2,3))> n^2由此看来终身教授也有智商拙计的时候呀。/***************QuickSort*******************/ //数组划分 int Partition( int arr[], int p, int r) { int key = arr[r]; int i = p - 1; for(int j = p; j < r; j++) { if( arr[j] <= key) { if( i%2 || (arr[j] < key) ) { //这里的if控制主要是在数组全部相等时使得 //函数返回(p+r)/2;对相等情况作了特别处理。 i++; swap( arr[j], arr[i]); } } } //cout <<i+1 <<":" <<arr[i+1] <<' ' <<r <<":" <<arr[r] <<endl; //Output(arr, 10 ); //cout <<(i+1 != r) <<endl; //if(i+1 != r) //cout <<i+1 << ' ' <<r <<endl; swap( arr[i+1], arr[r]); //Output(arr, 10 ); cout <<endl; return i+1; } void QuickSort( int arr[], int p,int r ) { int q; while( p < r ) { q = Partition( arr, p, r ); if( q-p < r-q ) { //总是递归划分后数组长度短的数组 //减少堆栈深度。 QuickSort( arr, p, q-1 ); p = q+1; } else { QuickSort( arr, q+1, r ); r = q-1; } } }
- 计数排序(CountingSort):第八章的算法都没有去实现,在这里用自己的语言去表达出其中的思想。计数排序虽然效率很高,但其局限性也很大,需要是整数而且不能过大,过大对空间要求太高,说白了就是个空间换去时间的经典算法。输入数组A[1···n],数组中的元素都小于一个整数k,用一个C[i]来记录下输入数组中小于等于i的元素个数,明显输这个记录小的那个就是比较大的元素,这样避免了元素间的比较。然后输出到数组B[]中,伪代码参上:
其实计数的过程是一个区间统计的问题,用到了树状数组的思想,在习题8.2-4就是一个经典的树状数组的应用。还有一个问题就是,以上伪代码的计数排序是个稳定排序,但如果你在最后一个循环for j = length[A] downto 1写成for j = 1 to length[A]那么这将不再是一个稳定排序。理由?自己想想。思考了下这个算法其实有很大的优化空间,如用离散技术吧待排序元素映射到一个比较小的区间等等。ConuntintSort() for i = 0;i to k; do c[i] = 0; for j = 1 t0 length[A]; do c[A[j] ] = C[A[j]]+1; //本身等于自己 for i = 1 to k ; do c[i] = c[i] + c[i-1];//关键 for j = length[A] downto 1 do B[C[A[j]] = A[j] C[A[j]] = C[A[j]] - 1
- 基数排序(RadixSort):这是个依赖于其他稳定排序的算法,思想是对数组中的每个数位进行排序。
- 桶排序(Buck Sort):大概是说有一个个已经有序的桶,你为待排序元素找到符合条件的桶丢进去。然后在对每个桶中的元素排序,完成了排序,只需输出数据就好。
- 插入排序[Inserttion Sort]: 对于输入数组A[n],n代表数组大小。算法总保持A[1···j-1] 区间内的元素已排好序,把a[j]插入到这个数组中使得a[1····j]是有序的,当j==n-1时,整个数组有序,排序完成,当已排好序数组元素只有一个,显然是有序的。就好像我们打牌时候摸牌的过程,总保持手上的牌是排好序的,新模上来的牌找到合适的位置插入,保持整个手牌有序,到最后也是有序的。 代码如下:
对于其他排序以后用到,或者对上述排序有什么新的理解,或者应用以及好的优化,再更新
THE END~~~