几大排序的稳定性
以前看人利用口诀记忆排序的稳定性:考研好痛苦,一堆(堆排序)专业课,情绪不稳定(不稳定排序),快(快速排序)来选(选择排序)一些(希尔排序)朋友聊聊天吧,剩下的都是稳定的。
感觉不错。。。。。。
接下来是八大排序总结 :
(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
1 void bubbleSort(int a[], int low, int high){//[low,high) 2 while(low < (high = bubble(a, low, high)));//逐趟扫描,直至全部有序 3 } 4 5 6 7 int bubble(int a[], int low, int high){ 8 int last = low; 9 while(++low<high) 10 if(a[low-1]>a[low]){ 11 swap(a[low-1],a[low]); 12 last = low;//找到上次最后一次发生交换的位置 13 } 14 return last; 15 }//平均复杂度太高了,不能用不能用。。。。。。
(2)选择排序
选择排序是给每个位置选择当前元素最小的,序列5 8 5 2 9,一轮下来变成2 8 5 5 9 。所以选择排序不是一个稳定的排序算法。
1 void select_sort(int arr[], int left, int right) { 2 for (int i = left; i < right; ++i) { 3 int min_index = i; 4 for (int j = i; j < right; ++j) { 5 if (arr[j] <= arr[min_index]) min_index = j; 6 } 7 std::swap(arr[min_index], arr[i]); 8 } 9 } // o(n^2) 复杂度较高不好用
(3)插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。这个排序也可以利用STL的sort来模拟,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。这个排序在元素个数较少或者序列基本有序的情况下速度是非常可观的,对一组序列如果数据量很大的时候可以主要用快排,当快排将区间划分的较小的时候切换排序方式为插入排序(如果快速排序递归栈过深也可采用堆排序避免)。
1 void insertSort(int arr[], int size){ 2 for (int i = 1; i < size; i++){ 3 if(arr[i] < arr[i - 1]){ 4 int temp = arr[i]; 5 int j; 6 for(j = i - 1; j >=0 && temp < arr[j]; --j){ 7 arr[j + 1] = arr[j]; 8 } 9 arr[j + 1] = temp; 10 } 11 } 12 }//元素基本有序的情况下,这个排序的复杂度很低,挺实用的,附上代码一份
(4)快速排序
简单来说就是选取一个中枢元素,一轮下来比中枢元素的小的在一边,比中枢元素大的在另外一边,所以每一轮可以将这一轮的中枢元素移动到其最终位置上(用此方法可以快速找到数组中第K个大小的元素而无需使用堆这一数据结构),这个
比如序列为5 6 3 4 3,以5为中枢元素,一轮下来该序列变成3 4 3 5 6,(5已经在最终位置上,但是两个3的相对位置发生了改变)。
1 void Qsort2(int arr[], int low, int high){ 2 if(low >= high) return; 3 int i = low; 4 int j = high; 5 int key = arr[low]; 6 while(i < j){ 7 while(i < j && key <= arr[j]) j--; 8 arr[i] = arr[j]; 9 while(i < j && key >= arr[i]) i++; 10 arr[j] = arr[i]; 11 } 12 arr[i] = key; 13 Qsort2(arr, low, i - 1); 14 Qsort2(arr, i + 1, high); 15 }//复杂度o(nlogn),速度很快但是递归过程中可能爆栈,总的来说还是非常实用的
1 void Qsort(int arr[], int l, int r) { 2 if (l >= r) return; 3 4 int i = l - 1; 5 int j = r + 1; 6 int x = arr[i + j >> 1]; 7 while (i < j) { 8 while (arr[++i] < x); 9 while (arr[--j] > x); 10 if (i < j) swap(arr[i], arr[j]); 11 } 12 Qsort(arr, l, j); 13 Qsort(arr, j + 1, r); 14 }
(5)归并排序
归并排序是针对两个已经排好序的序列,将这;两个序列合并成一个有序序列的排序方法,对于一个长度为n未排序的序列,我们一开始可以假设它是n个长度为1的有序序列构成,然后进行一次合并变成 n / 2 个长度为2的有序序列,然后再一次合并成
n / 4个长度为4的有序序列,以此类推。。。进行log2 (n)次后整个未排序序列就会变成一个长度为n的有序序列,时间复杂度为O(logn)
归并排序是稳定的排序算法。
1 /** 2 * Definition for singly-linked list. 3 * struct ListNode { 4 * int val; 5 * ListNode *next; 6 * ListNode(int x) : val(x), next(NULL) {} 7 * }; 8 */ 9 class Solution { 10 public: 11 ListNode* sortList(ListNode* head) { 12 int n{}; 13 for (auto p = head; p; p = p->next) ++n; 14 auto dummy = new ListNode(0); 15 dummy->next = head; 16 // i是每轮区间的长度 17 for (int i = 1; i < n; i <<= 1) { 18 auto cur = dummy; 19 // j是每轮排序的两个区间的第一个节点 20 for (int j = 0; j + i < n; j += i << 1) { 21 // left是左边起点, right是右边起点 22 auto left = cur->next, right = cur->next; 23 for (int k = 0; k < i; ++k) right = right->next; 24 // l是左边以及排好序的节点数,r是右边已经排好序的节点数 25 int l = 0, r = 0; 26 while (l < i && r < i && right) { 27 if (left->val < right->val) { 28 cur->next = left; 29 cur = left; 30 left = left->next; 31 l++; 32 } 33 else { 34 cur->next = right; 35 cur = right; 36 right = right->next; 37 r++; 38 } 39 } 40 while (l < i) { 41 cur->next = left; 42 cur = left; 43 left = left->next; 44 l++; 45 } 46 while (r < i && right) { 47 cur->next = right; 48 cur = right; 49 right = right->next; 50 r++; 51 } 52 cur->next = right; 53 } 54 } 55 56 return dummy->next; 57 } 58 }; // leetcode 148 链表的归并排序, 时间复杂度O(n), 空间复杂度O(1)
(6)基数排序 (桶排序)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
听说复杂度可以到o(n^1.3),没怎么见过人使用。
(8)堆排序
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, ... 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
可以用STL中make_heap来建堆并进行模拟,在复杂度和快速排序一样是o(nlogn)但是听说在排序过程中有较多的 << 1 和 >> 1操作,对CPU来说并不是非常友好,而快速排序都是++或者--操作,
所以STL的sort只有在递归栈过深的情况下才会使用堆排序,代码参见数据结构——堆的代码即可。
3 2 2 --------------------------------->(after sort)------------------------>2 2 3 (两个2的相对位置变了)
3 2
/ \ / \
2 2 2 3
// 0
// 1 2
// 3 4 5 6 ==> left_child = index * 2 + 1
inline int left_child(int index) { return index << 1 | 1; } void adjust_down(int arr[], int index, int size) { int child, temp; for (temp = arr[index]; left_child(index) < size; index = child) { child = left_child(index); // left child if (child != size - 1 && arr[child] < arr[child + 1]) { // has right child and left child // less than right child ++child; // make child point greater node } if (temp < arr[child]) { arr[index] = arr[child]; } else break; } arr[index] = temp; } // Max heap void heap_sort(int arr[], int size) { // make heap for (int i = (size >> 1) - 1; i >= 0; --i) { adjust_down(arr, i, size); } for (int i = size - 1; i > 0; --i) { int max = arr[0]; arr[0] = arr[i]; arr[i] = max; // swap(arr[0], arr[last]) adjust_down(arr, 0, i); } }
综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法
一道LeetCode上的排序题,这里采用插入排序加快速排序结合:
class Solution { public: //获取枢纽元素,这里不取第一个元素为枢纽元素,要避免这种愚蠢的做法 int median(vector<int> &arr, int left, int right) { int center = left + right >> 1; if(arr[center] < arr[left]) swap(arr[center], arr[left]); if(arr[right] < arr[left]) swap(arr[left], arr[right]); if(arr[right] < arr[center]) swap(arr[right], arr[center]); swap(arr[center], arr[right - 1]); return arr[right - 1]; } void quickSort(vector<int> &arr, int left, int right) { if(left + 10 <= right) { int pivot = median(arr, left, right); int i = left, j = right - 1; while(1) { while(arr[++i] < pivot); while(arr[--j] > pivot); if(i < j) swap(arr[i], arr[j]); else break; } swap(arr[i], arr[right - 1]); quickSort(arr, left, i - 1); quickSort(arr, i + 1, right); } else insertionSort(arr, left, right); } void insertionSort(vector<int> &arr, int left, int right) { for(int i = left + 1; i <= right; ++i) { auto tmp = arr[i]; int j; for(j = i; j > 0 && tmp < arr[j - 1]; --j) { arr[j] = arr[j - 1]; } arr[j] = tmp; } } vector<int> sortArray(vector<int>& nums) { quickSort(nums, 0, nums.size() - 1); return {nums}; } };
下面提供数据结构与算法分析C++版中的几大排序的源码
网址:http://users.cis.fiu.edu/~weiss/dsaa_c++4/code/
#ifndef SORT_H #define SORT_H /** * Several sorting routines. * Arrays are rearranged with smallest item first. */ #include <vector> #include <functional> using namespace std; /** * Simple insertion sort. */ template <typename Comparable> void insertionSort( vector<Comparable> & a ) { for( int p = 1; p < a.size( ); ++p ) { Comparable tmp = std::move( a[ p ] ); int j; for( j = p; j > 0 && tmp < a[ j - 1 ]; --j ) a[ j ] = std::move( a[ j - 1 ] ); a[ j ] = std::move( tmp ); } } /** * Internal insertion sort routine for subarrays * that is used by quicksort. * a is an array of Comparable items. * left is the left-most index of the subarray. * right is the right-most index of the subarray. */ template <typename Comparable> void insertionSort( vector<Comparable> & a, int left, int right ) { for( int p = left + 1; p <= right; ++p ) { Comparable tmp = std::move( a[ p ] ); int j; for( j = p; j > left && tmp < a[ j - 1 ]; --j ) a[ j ] = std::move( a[ j - 1 ] ); a[ j ] = std::move( tmp ); } } /** * Shellsort, using Shell's (poor) increments. */ template <typename Comparable> void shellsort( vector<Comparable> & a ) { for( int gap = a.size( ) / 2; gap > 0; gap /= 2 ) for( int i = gap; i < a.size( ); ++i ) { Comparable tmp = std::move( a[ i ] ); int j = i; for( ; j >= gap && tmp < a[ j - gap ]; j -= gap ) a[ j ] = std::move( a[ j - gap ] ); a[ j ] = std::move( tmp ); } } /** * Standard heapsort. */ template <typename Comparable> void heapsort( vector<Comparable> & a ) { for( int i = a.size( ) / 2 - 1; i >= 0; --i ) /* buildHeap */ percDown( a, i, a.size( ) ); for( int j = a.size( ) - 1; j > 0; --j ) { std::swap( a[ 0 ], a[ j ] ); /* deleteMax */ percDown( a, 0, j ); } } /** * Internal method for heapsort. * i is the index of an item in the heap. * Returns the index of the left child. */ inline int leftChild( int i ) { return 2 * i + 1; } /** * Internal method for heapsort that is used in * deleteMax and buildHeap. * i is the position from which to percolate down. * n is the logical size of the binary heap. */ template <typename Comparable> void percDown( vector<Comparable> & a, int i, int n ) { int child; Comparable tmp; for( tmp = std::move( a[ i ] ); leftChild( i ) < n; i = child ) { child = leftChild( i ); if( child != n - 1 && a[ child ] < a[ child + 1 ] ) ++child; if( tmp < a[ child ] ) a[ i ] = std::move( a[ child ] ); else break; } a[ i ] = std::move( tmp ); } /** * Internal method that makes recursive calls. * a is an array of Comparable items. * tmpArray is an array to place the merged result. * left is the left-most index of the subarray. * right is the right-most index of the subarray. */ template <typename Comparable> void mergeSort( vector<Comparable> & a, vector<Comparable> & tmpArray, int left, int right ) { if( left < right ) { int center = ( left + right ) / 2; mergeSort( a, tmpArray, left, center ); mergeSort( a, tmpArray, center + 1, right ); merge( a, tmpArray, left, center + 1, right ); } } /** * Mergesort algorithm (driver). */ template <typename Comparable> void mergeSort( vector<Comparable> & a ) { vector<Comparable> tmpArray( a.size( ) ); mergeSort( a, tmpArray, 0, a.size( ) - 1 ); } /** * Internal method that merges two sorted halves of a subarray. * a is an array of Comparable items. * tmpArray is an array to place the merged result. * leftPos is the left-most index of the subarray. * rightPos is the index of the start of the second half. * rightEnd is the right-most index of the subarray. */ template <typename Comparable> void merge( vector<Comparable> & a, vector<Comparable> & tmpArray, int leftPos, int rightPos, int rightEnd ) { int leftEnd = rightPos - 1; int tmpPos = leftPos; int numElements = rightEnd - leftPos + 1; // Main loop while( leftPos <= leftEnd && rightPos <= rightEnd ) if( a[ leftPos ] <= a[ rightPos ] ) tmpArray[ tmpPos++ ] = std::move( a[ leftPos++ ] ); else tmpArray[ tmpPos++ ] = std::move( a[ rightPos++ ] ); while( leftPos <= leftEnd ) // Copy rest of first half tmpArray[ tmpPos++ ] = std::move( a[ leftPos++ ] ); while( rightPos <= rightEnd ) // Copy rest of right half tmpArray[ tmpPos++ ] = std::move( a[ rightPos++ ] ); // Copy tmpArray back for( int i = 0; i < numElements; ++i, --rightEnd ) a[ rightEnd ] = std::move( tmpArray[ rightEnd ] ); } /** * Return median of left, center, and right. * Order these and hide the pivot. */ template <typename Comparable> const Comparable & median3( vector<Comparable> & a, int left, int right ) { int center = ( left + right ) / 2; if( a[ center ] < a[ left ] ) std::swap( a[ left ], a[ center ] ); if( a[ right ] < a[ left ] ) std::swap( a[ left ], a[ right ] ); if( a[ right ] < a[ center ] ) std::swap( a[ center ], a[ right ] ); // Place pivot at position right - 1 std::swap( a[ center ], a[ right - 1 ] ); return a[ right - 1 ]; } /** * Internal quicksort method that makes recursive calls. * Uses median-of-three partitioning and a cutoff of 10. * a is an array of Comparable items. * left is the left-most index of the subarray. * right is the right-most index of the subarray. */ template <typename Comparable> void quicksort( vector<Comparable> & a, int left, int right ) { if( left + 10 <= right ) { const Comparable & pivot = median3( a, left, right ); // Begin partitioning int i = left, j = right - 1; for( ; ; ) { while( a[ ++i ] < pivot ) { } while( pivot < a[ --j ] ) { } if( i < j ) std::swap( a[ i ], a[ j ] ); else break; } std::swap( a[ i ], a[ right - 1 ] ); // Restore pivot quicksort( a, left, i - 1 ); // Sort small elements quicksort( a, i + 1, right ); // Sort large elements } else // Do an insertion sort on the subarray insertionSort( a, left, right ); } /** * Quicksort algorithm (driver). */ template <typename Comparable> void quicksort( vector<Comparable> & a ) { quicksort( a, 0, a.size( ) - 1 ); } /** * Internal selection method that makes recursive calls. * Uses median-of-three partitioning and a cutoff of 10. * Places the kth smallest item in a[k-1]. * a is an array of Comparable items. * left is the left-most index of the subarray. * right is the right-most index of the subarray. * k is the desired rank (1 is minimum) in the entire array. */ template <typename Comparable> void quickSelect( vector<Comparable> & a, int left, int right, int k ) { if( left + 10 <= right ) { const Comparable & pivot = median3( a, left, right ); // Begin partitioning int i = left, j = right - 1; for( ; ; ) { while( a[ ++i ] < pivot ) { } while( pivot < a[ --j ] ) { } if( i < j ) std::swap( a[ i ], a[ j ] ); else break; } std::swap( a[ i ], a[ right - 1 ] ); // Restore pivot // Recurse; only this part changes if( k <= i ) quickSelect( a, left, i - 1, k ); else if( k > i + 1 ) quickSelect( a, i + 1, right, k ); } else // Do an insertion sort on the subarray insertionSort( a, left, right ); } /** * Quick selection algorithm. * Places the kth smallest item in a[k-1]. * a is an array of Comparable items. * k is the desired rank (1 is minimum) in the entire array. */ template <typename Comparable> void quickSelect( vector<Comparable> & a, int k ) { quickSelect( a, 0, a.size( ) - 1, k ); } template <typename Comparable> void SORT( vector<Comparable> & items ) { if( items.size( ) > 1 ) { vector<Comparable> smaller; vector<Comparable> same; vector<Comparable> larger; auto chosenItem = items[ items.size( ) / 2 ]; for( auto & i : items ) { if( i < chosenItem ) smaller.push_back( std::move( i ) ); else if( chosenItem < i ) larger.push_back( std::move( i ) ); else same.push_back( std::move( i ) ); } SORT( smaller ); // Recursive call! SORT( larger ); // Recursive call! std::move( begin( smaller ), end( smaller ), begin( items ) ); std::move( begin( same ), end( same ), begin( items ) + smaller.size( ) ); std::move( begin( larger ), end( larger ), end( items ) - larger.size( ) ); /* items.clear( ); items.insert( end( items ), begin( smaller ), end( smaller ) ); items.insert( end( items ), begin( same ), end( same ) ); items.insert( end( items ), begin( larger ), end( larger ) ); */ } } /* * This is the more public version of insertion sort. * It requires a pair of iterators and a comparison * function object. */ template <typename RandomIterator, typename Comparator> void insertionSort( const RandomIterator & begin, const RandomIterator & end, Comparator lessThan ) { if( begin == end ) return; RandomIterator j; for( RandomIterator p = begin+1; p != end; ++p ) { auto tmp = std::move( *p ); for( j = p; j != begin && lessThan( tmp, *( j-1 ) ); --j ) *j = std::move( *(j-1) ); *j = std::move( tmp ); } } /* * The two-parameter version calls the three parameter version, using C++11 decltype */ template <typename RandomIterator> void insertionSort( const RandomIterator & begin, const RandomIterator & end ) { insertionSort( begin, end, less<decltype(*begin )>{ } ); } #endif