排序算法作为一项需求,足够简单,是学习基础算法思想(分治算法、减治算法、递归)很好的学习材料。面试遇到写排序算法,先问清楚数据的特点,有些时候可能还会给具体的业务场景。
时间复杂度
-
时间复杂度统计的是算法的计算操作数量,而不是运行的绝对时间。两者呈正相关,并不相等。算法运行时间受编程语言、计算机处理速度、运行环境等多种因素的影响。
-
体现的是计算操作随数据大小N变化时的变化情况。
算法基础知识铺垫
- 稳定排序:如果a原本在b的前面,且a == b,排序之后a仍然在b的前面,则为稳定排序。冒泡排序、插入排序、归并排序
- 非稳定排序:如果a原本在b的前面,且a == b,排序之后a可能不在b的前面,则为非稳定排序。快速排序、选择排序、希尔排序、堆排序
- 原地排序:在排序过程中不申请多余的存储空间,只利用原来的存储空间进行比较和交换的数据排序。
- 非原地排序:需要利用额外的数组来辅助排序。
- 时间复杂度:一个算法执行所消耗的时间。
- 空间复杂度:运行完一个算法所需的内存大小。
1、冒泡排序(了解)
冒泡排序就是把小的元素往前调或者把大的元素往后调,冒泡排序是一种稳定排序算法
-
一般思路:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始的第一对到最后一对。这步做完以后,最后的元素会是最大的数。
- 针对所有元素重复以上步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤。
- 时间复杂度o(n^2),空间复杂度o(1)。
实现代码
void bubbleSort(vector<int>& a) { int n = a.size(); for (auto i = 0; i < n; ++i) { for (int j = 0; j < n - i - 1; ++j) { if (a[j] > a[j + 1]) swap(a[j], a[j + 1]); } } }
-
冒泡排序优化
- 假如从开始第一对到结尾最后一对,相邻元素之间都没有发生交换的操作,这意味着右边的元素总是大于等于左边的元素,此时的数组已经是有序的了
2、选择排序(了解)
选择排序是给每个位置选择当前元素最小值。
- 从待排序的数据中寻找最小值,将其与序列最左边起始位置的数字进行交换。
- 从剩余未排序元素中继续寻找最小值,然后放到序列最左边第二个位置。
- 以此类推,直到所有元素均排序完毕。
- 时间复杂度o(n^2),空间复杂度o(1)。
void selectSort(vector<int>& nums) {
int len = nums.size();
int minIndex = 0;
for (int i = 0; i < len; ++i) {
minIndex = i;
for (int j = i + 1; j < len; ++j) {
if (nums[j] < nums[minIndex]) minIndex = j;
}
swap(nums[i], nums[minIndex]);
}
}
3、归并排序(重点)
待补充
4、快速排序(重点)
-
快速排序原理
两个核心,分别是哨兵划分和递归
- 哨兵划分:以数组某个元素(一般选取首元素)为基准数,将所有小于基准数的元素移动至其左边,大于其基准数的元素移动至其右边
- 递归
-
快速排序和二分法的原理类似
class Solution { public: vector<int> getLeastNumbers(vector<int>& arr, int k) { quickSort(arr, 0, arr.size() - 1); vector<int> res; // 将arr容器中的[begin, end)中的元素赋值给res res.assign(arr.begin(), arr.begin() + k); return res; } private: void quickSort(vector<int>& arr, int l, int r) { // 子数组长度为 1 时终止递归 if (l >= r) return; // 哨兵划分操作(以 arr[l] 作为基准数) int i = l, j = r; while (i < j) { while (i < j && arr[j] >= arr[l]) j--; while (i < j && arr[i] <= arr[l]) i++; swap(arr[i], arr[j]); } swap(arr[i], arr[l]); // 递归左(右)子数组执行哨兵划分 quickSort(arr, l, i - 1); quickSort(arr, i + 1, r); } };
5、插入排序(熟悉)
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。
当然,刚开始这个有序的小序列只有一个元素,就是等于第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起。如果碰到一个和插入元素相等的,那么把想要插入的元素放在相等元素的后面。
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序元素中从后向前排序。
- 如果该元素(已排序)大于新元素,将该元素移到下一个位置。
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置后。
- 重复步骤2~5.
- 时间复杂度o(n^2),空间复杂度o(1)。
void print(vector<int>& a, int n, int i) {
cout << "step"<< i << ": ";
for (int j = 0; j < n; j++) {
cout << a[j] << " ";
}
cout << endl;
}
void insertionSort(vector<int>& a, int n) {//{ 9,1,5,6,2,3 }
for (int i = 1; i < n; ++i) {
if (a[i] < a[i - 1]) { //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
int j = i - 1;
int x = a[i]; //复制为哨兵,即存储待排序元素
//a[i] = a[i - 1]; //先后移一个元素,可以不要这一句,跟循环里面的功能重复了
while (j >= 0 && x < a[j]) { //查找在有序表的插入位置,还必须要保证j是>=0的因为a[j]要合法
a[j + 1] = a[j];
j--; //元素后移
}
a[j + 1] = x; //插入到正确位置
}
print(a, n, i); //打印每趟排序的结果
}
}
6、堆排序(堆很重要,堆排序看个人情况)
代补充
7、桶排序(了解)
待补充
8、基数排序(了解)
待补充
9、计数排序(了解)
待补充
10、希尔排序(不建议多花时间)
- 快速排序首先会在序列中随机选择一个基准值(pivot),然后将除了基准值以外的数分为“比基准值小的数”和“比基准值大的数”这两个类别,再将其排列成==【比基准值小的数】基准值【比基准值大的数】==形式。
- 快速排序是一种分治法。它将原本的问题分成两个子问题(比基准值小的数和比基准值大的数),然后再分别解决这两问题。
- 解决子问题的时候会再次使用快速排序,甚至这个快速排序里面仍然要使用快速排序。
- 分割子序列时候需要选择基准值,如果每次选择的基准值都能使两个子序列的长度为原来的一半,那么快速排序的运行时间为nlogn。