我们学习过很多排序算法,但是我们又很难将它们记住,当面试的时候问到其中的一个排序算法的时候,或者需要解决某些问题的时候,我们需要选择一些排序算法。如果我们此时不熟悉常见排序的特点,那么我们真的会很难受的,我感觉我就经历过这种事。所以我觉得我有必要重新认识一下常见排序算法并将它们总结一下。好,闲话不扯了,我们开始吧。
1.冒泡排序
代码:
void BubbleSort(int* arr, int size) { assert(arr && size > 0); for (int i = 0; i < size - 1; ++i) { int flag = 0; //用于优化该排序 for (int j = 0; j < size - i - 1; ++j) { if (arr[j + 1] < arr[j]) { swap(arr[j + 1], arr[j]); flag = 1; } } if (flag == 0) return; } }---时间复杂度 O(n^2),没有消耗空间
---请仔细看看以上代码,看看边界的控制,自己理解一下吧,这里我写的是优化版的。
2.插入排序
代码:
void InsertSort(int* arr, int size) { assert(arr && size > 0); for (int i = 1; i < size; ++i) { int tmp = arr[i]; //暂时存放该值 int j = i-1; while (j >= 0 && arr[j] >= tmp) { arr[j + 1] = arr[j]; --j; } arr[j+1] = tmp; } }---时间复杂度 O(n^2),没有消耗空间
3.选择排序
代码:
void SelectSort(int *arr,int size) { assert(arr && size > 0); int left = 0; int right = size - 1; while (left < right) { int max = left; int min = left; for (int i = left + 1; i <= right; ++i) { if (arr[i]>arr[max]) max = i; if (arr[i] < arr[min]) min = i; } if (min == right && max == left) //此种情况交换两次会导致错误(依次递减) { swap(arr[min], arr[max]); ++left; --right; continue; } if (min != left) { swap(arr[left], arr[min]); } if (max != right) { swap(arr[right], arr[max]); } ++left; --right; } }---时间复杂度 O(n^2),没有消耗空间 ---这个排序可以每次找出最大和最小的两个数,这样遍历次数减小一半,不过这样需要注意一种特殊情况就是,上面我的代码中写了注释的地方,其他的则不易出错
4.堆排序
代码:
void AdjustDown(int* arr, int root, int size) { int begin = root; int left = 2 * root + 1; int right = left + 1; while (left < size) { int max = left; if (right<size && arr[right]>arr[max]) { max = right; } if (arr[begin] < arr[max]) //根比叶子小,则交换 { swap(arr[begin], arr[max]); } else //否则,退出 { break; } begin = max; left = begin * 2 + 1; right = left + 1; } } void HeapSort(int* arr, int size) { assert(arr && size > 0); //建堆 int root = size / 2 - 1; while (root >= 0) { AdjustDown(arr, root, size); --root; } //排序 int begin = size-1; while (begin) { swap(arr[0], arr[begin]); AdjustDown(arr, 0, begin); --begin; } }---时间复杂度 O( n(lg N) ),没有消耗空间
(1). 建堆,(其数组看成完全二叉树),从最后一个"根节点"起,依次向下调整,调整完以后最大堆或最小堆就建立起来了
(2).每次将筛选出来的最大值或最小值放到数组最后一位(其实是将数组第一位与最后一位交换),然后将堆的规模减小1,当规模为1的时候,数组里面就是排好序的了。
(3). 其实堆排序最重要的是用来找出k个最小数或k个最大数。思路:简历堆,然后遍历数组,置换出堆中的数据,遍历完的时候堆中的数据就是索要求的结果了。至于创建最大堆还是最小堆,这有个技巧,就是与你所求的相,即:找最小用打大堆,找最大用小堆。最后注意堆的使用场合。
5.希尔排序
代码:
void ShellSort(int * arr, int size) { assert(arr && size > 1); int gap = size; while (gap > 1) { gap = gap / 3 + 1; //选择gap for (int i = 0; i < gap; ++i) { for (int j = i+gap; j < size; j = j + gap) //以gap为间隔插入排序 { int tmp = arr[j]; int k = j - gap; while (k>=0 && arr[k]>tmp) { arr[k + gap] = arr[k]; k -= gap; } arr[k + gap] = tmp; } } } }---时间复杂度不确定,据统计在O(n^1.25)~O(1.6n^1.25)之间,没有消耗空间 ---希尔排序的思想是缩小增量排序,注意gap的取法对时间复杂度有影响,像上面这样取gap是比较好的做法
*6.快速排序
代码:
void QuickSort1(int* arr, int size) //一般方式 { assert(arr); if (size <= 1) { return; } int key = arr[size - 1]; //以最后一个元素为key值 int begin = 0; int end = size - 2; while (begin < end) { while (begin<end && arr[begin] <= key) //找大 { ++begin; } while (end>begin && arr[end] >= key) //找小 { --end; } if (arr[begin] > arr[end]) { swap(arr[begin], arr[end]); ++begin; --end; } } if (begin==end && arr[begin] < key) //避免逆序时出错 { begin++; } if (begin != size - 1) //若它和最后一个重合了,就不交换了 { swap(arr[begin], arr[size - 1]); } QuickSort1(arr, begin); //左边递归排序,begin此时代表数组元素的个数 QuickSort1(arr + begin + 1, size - begin - 1); //右边递归排序 }
int MidValue(int* arr, int size) { if (size == 1) return 0; int left = arr[0]; int right = arr[size - 1]; int mid = arr[(size - 1) / 2]; if (left > right) { if (mid>left) return 0; else { if (mid < right) return size-1; else return (size-1)/2; } } else { if (mid>right) return (size - 1); else { if (mid < left) return 0; else return (size - 1) / 2; } } } void QuickSort2(int* arr, int size) //三数取中法 { assert(arr); if (size <= 1) return; int pos = MidValue(arr, size); swap(arr[pos], arr[size - 1]); int key = arr[size - 1]; int begin = 0; int end = size - 2; while (begin < end) { while (begin<end && arr[begin] <= key) { ++begin; } while (end>begin && arr[end] >= key) { --end; } if (arr[begin] > arr[end]) { swap(arr[begin], arr[end]); ++begin; --end; } } if (begin == end && arr[begin] < key) { ++begin; } if (begin != size-1) swap(arr[begin], arr[size-1]); QuickSort2(arr, begin); //begin此时代表数组元素的数目,左边递归排序 QuickSort2(arr + begin + 1, size - begin - 1); //右边递归排序 }
void QuickSort3(int* arr, int size) //非递归实现快速排序 { assert(arr); if (size <= 1) return; stack<int> sk; sk.push(0); sk.push(size-1); //压栈的是数组的下标,与上面的不同,需要注意 while (!sk.empty()) { int last = sk.top(); sk.pop(); int first = sk.top(); sk.pop(); int begin = first; int end = last-1; int key = arr[end+1]; while (begin < end) { while (begin < end && arr[begin] <= key) { ++begin; } while (end>begin && arr[end] >= key) { --end; } if (arr[begin] > arr[end]) swap(arr[begin], arr[end]); } if (begin == end && arr[begin] < key) begin++; if (begin != end + 1) swap(arr[begin], arr[last]); int begin1 = first; int end1 = begin; if (end1 - begin1 > 1) { sk.push(begin1); sk.push(end1-1); //此时压的是下标 } int begin2 = begin + 1; int end2 = last; if (end2 - begin2 > 1) { sk.push(begin2); sk.push(end2); //此时压的是下标 } } }
void QuickSort4(int* arr, int size) //可用于数组和链表的快排 { assert(arr); if (size <= 1) return; int pos = MidValue(arr, size); //size表示元素个数 swap(arr[pos], arr[size - 1]); int key = arr[size - 1]; int prev = -1; int cur = 0; while (cur < size) { if (arr[cur] < key) { ++prev; if (prev != cur) swap(arr[prev], arr[cur]); } ++cur; } swap(arr[++prev],arr[size-1]); QuickSort4(arr,prev); QuickSort4(arr+prev+1,size-prev-1); }
void QuickSort5(int* arr, int size) //按区间排序,小于13和大于13 { assert(arr); if (size <= 1) return; if (size < 13) {//插入排序 InsertSort(arr, size); } else {//快速排序 QuickSort1(arr, size); } }---时间复杂度O(n(lg N) ),没有消耗空间 ---快排算的上是最有名的排序算法,上面写了四种快速排序,可见它的重要性以及复杂性,可以这样会说快排就像开车一样,不会开的人开再好的也没用,会的人能充分的利用它。下面我就简单谈谈吧
(1). 快速排序的key值的选择,选择太小或太大都会使整体偏低,所以我给出了三数取中法。
(2). 快速排序的区间选择很重要,区间大于13适合使用快排,区间小于13使用插入排序,这样的效率比较高(是经过测试得出的)
(3). 快速排序还能用于链表的排序,这点很重要
7.归并排序
代码:
void doMerge(int* newArr, int left, int right) { assert(newArr); if (left == right) return; int mid = left + (right - left) / 2; doMerge(newArr, left, mid); doMerge(newArr, mid + 1, right); for (int i = left + 1; i <= right; ++i) { int tmp = newArr[i]; int end = i-1; while (end >= left && newArr[end] > tmp) { newArr[end+1] = newArr[end]; --end; } newArr[end+1] = tmp; } } void Merge(int* arr, int* newArr, int left, int mid, int right) { assert(arr); assert(newArr); if (left == right) return; int begin1 = left; int end1 = mid; int begin2 = mid + 1; int end2 = right; int pos = 0; while (begin1 <= end1 && begin2 <= end2) { if (newArr[begin1] <= newArr[begin2]) { arr[pos++] = newArr[begin1++]; } else { arr[pos++] = newArr[begin2++]; } } while (begin1 <= end1) { arr[pos++] = newArr[begin1++]; } while (begin2 <= end2) { arr[pos++] = newArr[begin2++]; } } void MergeSort(int* arr, int size) { assert(arr); if (size <= 1) return; int left = 0; int right = size - 1; int mid = (size-1) / 2; int * newArr = new int[size]; memcpy(newArr, arr, sizeof(int)*size); assert(newArr); doMerge(newArr, left, mid); doMerge(newArr, mid + 1, right); Merge(arr, newArr, left, mid, right); }---时间复杂度O(n(lg N) ),空间复杂度O(n)
---归并排序是一种空间换取时间的排序,我们需要理解归并排序的思想,即分治的思想
8.计数排序
代码:
void CountingSort(int* arr, int size) //计数排序 {//数据集中的的时候很好 assert(arr); if (size <= 1) return; int min = arr[0]; int max = 0[arr]; for (int i = 1; i < size; ++i) { if (arr[i]>max) max = arr[i]; if (arr[i] < min) min = arr[i]; } //找打区间 int arrSize = max - min + 1; int* newArr = new int[arrSize]; memset(newArr, 0, sizeof(int)*arrSize); for (int i = 0; i < size; ++i) { ++newArr[arr[i] - min]; } for (int j = 0 , i=0; j < arrSize; ++j) { while (newArr[j] != 0) { arr[i++] = j + min; --newArr[j]; } } }---时间复杂度O(n),空间复杂度O(n)
---这种排序是一种典型的空间换取时间的排序,若数字的分布区间较小,非常合适,因为额外开辟的数组就是数字区间的大小,所以当数字的分布区间较大的时候就不适合了
9.基数排序
代码:
int digData(int data, int dig) { while (--dig) { data /= 10; } return data % 10; } void radixSort(int* arr, int left, int right, int dig) { if (left>=right || dig < 1) return; int* count = new int[10]; int* newArr = new int[right - left + 1]; memset(count, 0, sizeof(int)* 10); memset(newArr, 0, sizeof(int)* (right - left + 1)); for (int i = left; i <= right; ++i) //统计每位的元素的个数 { ++count[digData(arr[i], dig)]; } for (int i = 1; i < 10; ++i) //记录新的以为元素的开始地址 { count[i] = count[i] + count[i - 1]; } for (int i = left; i <= right; ++i) //将排序后的元素写入辅助数组newArr中 { int pos = digData(arr[i], dig); newArr[--count[pos]] = arr[i]; } int j = 0; for (int i = left; i <= right; ++i, ++j) //将排序好的元素写回arr中 { arr[i] = newArr[j]; } for (int i = 0; i < 10; i++) { int first = count[i]; int last = count[i + 1] - 1; radixSort(arr, first, last, dig - 1); } } void RadixSort(int* arr, int size,int dig) { assert(arr); if (size <= 1 || dig < 1) return; int left = 0; int right = size - 1; radixSort(arr, left, right, dig); }---时间复杂度O(d(n+radix)),空间复杂度O(n)
---按位来排序,这个排序算法的时间复杂度也是比较低的,可以好好研究一下。
打印数组函数:
//打印数组 void PrintArr(int* arr, int size) { assert(arr && size>0); for (int i = 0; i < size; ++i) { cout << arr[i] << " "; } cout << endl; }
int main() { //4个测试用例 //int array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int array[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; //int array[10] = { 0, 1, 5, 3, 7, 6, 2, 4, 8, 9 }; //int array[10] = { 1, 3, 5, 0, 7, 6, 9, 8, 4, 2 }; //int arrayTest[15] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ,23,45,32,77,29}; //BubbleSort(array, 10); //InsertSort(array, 10); SelectSort(array, 10); //HeapSort(array, 10); //ShellSort(array, 10); //快速排序 //数组形式newArr[]的快排 //QuickSort1(array, 10); //QuickSort2(array, 10); //QuickSort3(array, 10); //可用于数组和链表的快排 //QuickSort4(array, 10); //优化的快速排序 // QuickSort5(arrayTest, 15); //PrintArr(arrayTest, 15); //QuickSort5(array, 10); //MergeSort(array, 10); //RadixSort(array, 10, 1); //CountingSort(array, 10); PrintArr(array, 10); //int array[12] = {332,633,59,589,232,664,179,457,825,714,405,361}; //基数排序数组 //RadixSort(array, 12, 3); //PrintArr(array, 12); return 0; }