排序算法总结(C++)(第一部分)
在这篇文章中,记录了自己学习的经典排序算法,代码语言用的C++。现阶段,排序算法已经非常成熟,有各种封装的库函数。但是还是要学习并自己写出代码,一方面理解排序算法可以帮助我们锻炼思维能力,另一方面自己给出代码可以增加我们的码农能力。而在这里记录总结下来,一方面和大家分享探讨,另一方面有不对的地方,希望大家给我指导改正。此外,也能锻炼自己的学习总结能力。
本博客基本内容分为两部分:
- 第一部分:基于比较的排序算法
- 第二部分:不基于比较的排序算法
1.冒泡排序
这是最简单直接的方法。
- 从数组左边到右边,依次两两比较,将较大的交换到后面,直到数组末尾位置。
- 在从剩余未排序元素中寻找最大值。每次都保证前N个数最大的一个数在第N-1个位置,直到数组最后。
- 最大的数就像气泡一样,依次冒到最后的位置。
时间复杂度O(n^2),空间复杂度O(1)
属于稳定的排序算法。
void bubbleSort(vector<int>&arr) { for (int i = 0; i < arr.size() - 1; i++){ for (int j = 0; j < arr.size() - i -1; j++) { if(arr[j] > arr[j+1]) swap(arr, j, j+1); } } } } void swap(vector<int>&arr, int i, int j){ int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
2.选择排序
选择排序是一种简单直观的的排序算法。基本思路和冒泡排序看似一样。
- 数组从左到右,两两比较,但不进行交换,使用一个变量记录最大值所在数组的下标,向左依次移动,直到末尾
- 将最大值与数组末尾交换
- 在从剩余未排序元素中继续寻找最大值,放到排序好序列的前一个位置
时间复杂度O(n^2),空间复杂度O(1)
属于不稳定的排序算法
注意:动图中是选择的最小的往前排,和我介绍的思路一样。
void selectionSort(){ int length = sizeof(arr) / sizeof(arr[0]); if(length < 0) return; for(int i = 0; i < length; ++i){ int minValue = arr[i]; int index = i; for(int j = i + 1; j < length; ++j){ if(minValue > arr[j]){ index = j; minValue = arr[j]; } } if(index != i){ swap(arr, index, i); } } } void swap(vector<int> &arr, int i, int j){ int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
3.插入排序
人如其名,算法也是。将一个个数依次插入到已经排序好的数组中,这是插入排序的思路。
- 数组中前n个数已经排序好,将第n+1个数M插入到已排序好的数组中
- 将M与前一个数比较,如果M小,则与前一个数交换
- 依次与前一个数进行比较,直到M大于等于前一个数,停止,排序完成
时间复杂度O(n^2),空间复杂度O(1)
void insertSort(){ int length = sizeof(arr) / sizeof(arr[0]); if(arr == NULL || length < 2) return; for(int i = 0; i < length; ++i){ for(int j = i - 1; j >= 0 && arr[j] > arr[j+1]; --j) { swap(arr, j, j+1); } } } void swap(vector<int> &arr, int i, int j){ int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
4.归并排序
归并排序采用的是分而治之的思想。有位伟人说过,大事化小小事化了。开个玩笑!其实就是将一个大问题转化成一个个小问题,小问题解决,大问题也就解决了。
- 将一个未排序的数组分为两个子数组
- 两个子数组分别排好序,然后合并成一个有序的数组
那么两个子数组怎么排好序呢?没错!在分别分为两个子子数组,然后一直分一直分一直分...直到只有一个数,那么这一个数肯定是排好序的,然后向上合并,一直合并一直合并,直到原始数组大小。那么这个排序的关键点就在怎么合并成一个有序数组。
两个数组a和b,分别都已经排好序
准备一个辅助数组c
a[0] 与 b[0]进行比较,较小数(假如是[a0]较小)放在c[0]位置,a向左移动一个位置
a[1] 与 b[0]比较,依次进行,直到有一个数组比较完
* 另一个数组剩下的数拷贝到c数组中
时间复杂度O(nlogn),空间复杂度O(n)
属于稳定的排序算法
void mergeSort(){ int length = sizeof(arr) / sizeof(arr[0]); if(arr == NULL || length < 2) return; mergeMain(arr, 0, length); } //用的是递归 void mergeMain(int arr[], int L, int R){ if(L == R) return; int mid = L + ((R - L) >> 1); mergesort(arr, L, mid); mergesort(arr, mid + 1, R); merge(arr, L, mid, R); } void merge(int arr[], int l, int m, int r){ int help[l-r+1]; int i = 0; int p1 = l; int p2 = m + 1; while (p1 <= m && p2 <= r) { help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; } while (p1 <= m) { help[i++] = arr[p1++]; } while (p2 <= r) { help[i++] = arr[p2++]; } for(int i = 0; i < l - r + 1; i++){ arr[i + l] = help[i]; } }
5.快速排序
简称快排。C++的库函数中有快排的函数,可见快排是挺靠谱的。基本思路就是,将数组分为两个部分,前一部分比某个数M要小,另一部分比M要大,分别对这两个部分依次进行这样的操作,最后达到整个数组的排序。
掌握快排要掌握两个关键点:
- 第一个关键点:怎么寻找这个数M,寻找方法的好坏之间关系到排序算法的时间复杂度。
- 第二个关键点:怎么将一个数组小的放在左边,大的放在右边,等于的放在中间。
先介绍第二个关键点的问题,其实就是荷兰国旗问题。
现有红白蓝三个不同颜色的小球,乱序排列在一起,请重新排列这些小球,使得红白蓝三色的同颜色的球在一起。
这个问题之所以叫荷兰国旗问题,是因为我们可以将红白蓝三色小球想象成条状物,有序排列后正好组成荷兰国旗。
我们将这个问题转化成排序问题,即红白蓝分为对应数字0,1,2,那么就是小于1的放在左边,大于1的放在右边,等于1的放在中间。
/* 在arr上的l到r位置,小于num的放在左边,大于num的放在右边,等于num的放在中间 less代表小于边界,more代表大于边界 有3种情况需要考虑,具体解释看下一个代码 */ void NetherlandFlag(vector<int> &arr, int num, int l, int r){ if(l >= r){ return; } int less = l - 1; int more = r + 1; int cur = l; while(cur < more){ if(arr[cur] < num){ swap(arr, ++less, cur++); } else if(arr[cur] > num){ swap(arr, --more, cur); } else{ cur++; } } } void swap(vector<int> &arr, int i, int j){ int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
关于第一个关键点,其实就是随机选择一个数,选择数的好坏,将会影响排序的时间复杂度。
时间复杂度O(n*logn),空间复杂度O(logn)
属于不稳定的排序算法
/* 划分函数,类似于荷兰国旗问题 有3个指针,less代表小于的边界,more代表大于的边界,cur代表现在的位置,代码中用L代替 当前数<num时,less右边一个位置和当前位置交换,并同时向右移动一个位置 当前数>num时,more左边一个位置和当前位置交换,more向左移动一个位置,当前位置不变 当前书=num时,当前位置向右移动一个位置 最终,小于num的在左边,大于num的在右边,等于num的在中间 */ int* partition(vector<int> &arr, int L, int R){ int less = L - 1; int more = R; while(L < more){ if(arr[L] < arr[R]){ swap(arr, ++less, L++); } else if(arr[L] > arr[R]){ swap(arr, --more, L); } else{ L++; } } int *p = new int[2]; swap(arr, L, R); p[0] = less; p[1] = more + 1; return p; } /* 递归函数 随机找到数组中一个数作为划分点 取得随机整数[a, b]函数为 rand()%(b-a+1)+a */ void sort(vector<int> &arr, int L, int R){ swap(arr, rand() % (R-L+1) + L, R); if(L < R){ int* p = new int[2]; p = partition(arr, L, R); sort(arr, L, p[0]); sort(arr, p[1], R); } } void swap(vector<int> &arr, int i, int j){ int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
6.堆排序
要弄清楚什么叫堆排序,首先要理解什么叫堆,什么叫大根堆,小根堆。
堆是一个完全二叉树,其实堆是一个概念,而不是一个物理结构,物理结构可以用数组,来表达堆。
要记住两个关键点:
- 节点下标i的左孩子节点下标是(2×i+1),右孩子节点下标是(2×i+2)
- 节点下标i的父节点下标是(i-1)/2
时间复杂度O(n*logn),空间复杂度O(1)
属于不稳定的排序算法
/* 堆排序算法 首先把数组调整成大根堆(注:大根堆只是逻上的概念,并不是一个确定的数据结构) 定义大根堆尺寸,即heapSize 将堆顶元素(即数组为0的位置)与大根堆最一个元素(即数组最后一个位置)交换,尺寸一,heapSize-- 此时,堆并不是一个大根堆,重新调整为大根堆 依次循环进行,直到heapSize<=0,排序结束 */ void heapSort(vector<int> &arr){ int length = arr.size(); if(arr == NULL || length < 2){ return; } for(int i = 0; i < length; i++) { heapInsert(arr, i); } swap(arr, 0, --length); while(length > 0){ heapify(arr, 0, length); swap(arr, 0, --length); } } /* 大根堆插入元素之后,重新调整为大根堆的函数 */ void heapInsert(vector<int> &arr,int index){ while(arr[index] > arr(index-1)/2]){ swap(arr, index, (index-1/2); index = (index-1) / 2; } } /* 将堆顶元素弹出,重新调节为大根堆的函数 */ void heapify(vector<int> &arr, intindex, int heapSize){ int left = 2 * index + 1; while(left < heapSize){ int largest = left+1 <heapSize && arr[left] < ar[left+1] ? left+1: left; largest = arr[index] < ar[largest] ? largest :index; if(largest == index){ break; } swap(arr, index, largest); index = largest; left = 2 * index + 1; } } void swap(vector<int> &arr, int i,int j){ int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
个人声明
由于本人水平有限,也是刚学习,其中不免出现错误,还请各位看官不吝赐教,万分感谢!
而且本篇也算是学习笔记,是看视频上课的知识点