排序
1 冒泡排序
1. 原理: 比较两个相邻的元素,将值大的元素交换到右边
2. 思路: 依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。
(1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。
(2) 比较第2和第3个数,将小数 放在前面,大数放在后面。
…
(3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成
(4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。
(5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。
(6)依次类推,每一趟比较次数减少依次
3. 举例
4. 代码实现
void BubbleSort(int arr[], int a){
if(arr == NULL || a < 0) return;
int temp;
for(int i = 0; i < a; i++){
for(int j = i + 1; j < a; j++){
if(arr[i] > arr[j]){
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
print(arr, a);
}
2 选择排序
1. 概念 选择排序(Select Sort)是直观的排序,通过确定一个 Key 最大或最小值,再从带排序的的数中找出最大或最小的交换到对应位置。再选择次之。双重循环时间复杂度为 O(n^2)
2. 算法描述:
在一个长度为 N 的无序数组中,第一次遍历 n-1 个数找到最小的和第一个数交换。
第二次从下一个数开始遍历 n-2 个数,找到最小的数和第二个数交换。
重复以上操作直到第 n-1 次遍历最小的数和第 n-1 个数交换,排序完成。
3. 代码实现
void SelectSort(int arr[], int a){
if(arr == NULL || a <= 0) return;
for(int i = 0; i < a; i++){
int MinKey = arr[i];
for(int j = i; j < a; j++){
if(MinKey > arr[j]){
int temp = arr[j];
arr[j] = MinKey;
MinKey = temp;
}
}
arr[i] = MinKey;
}
}
3 插入排序
1. 实现思路:
- 从数组的第二个数据开始往前比较,即一开始用第二个数和他前面的一个比较,如果 符合条件(比前面的大或者小,自定义),则让他们交换位置。
- 然后再用第三个数和第二个比较,符合则交换,但是此处还得继续往前比较,比如有 5个数8,15,20,45, 17,17比45小,需要交换,但是17也比20小,也要交换,当不需 要和15交换以后,说明也不需要和15前面的数据比较了,肯定不需要交换,因为前 面的数据都是有序的。
- 重复步骤二,一直到数据全都排完。
2. 代码实现
void InsertSort(int a[], int n){ //直接插入排序
int i,j,temp=0;
for(i=1;i<n;i++){
if(a[i]<a[i-1]){
temp = a[i];
for(j=i-1;j>=0 && a[j]>temp;j--) {
a[j+1]=a[j];
}
a[j+1]=temp;
}
}
}
4 快排
1. 基本概念
快速排序(Quick Sort)使用分治法策略。它的基本思想是:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
2. 算法流程:
- (1) 从数列中挑出一个基准值。
- (2) 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
- (3) 递归地把"基准值前面的子数列"和"基准值后面的子数列"进行排序。
3. 图解
4. 随机快排
经典快速排序总是指定数组或者某部分的最后一个元素作为基准值,随机快速排序指定数组或者某一部分中的随机值作为基准值。快排随机版本只需要 先使用rand()方法找到一个随机下标,接着swap(nums[left], nums[i]),然后和普通版本代码一模一样
5. 代码实现
int findKthLargestQuick(vector<int>& nums, int k, int left, int right){
// 快排思想
int n = nums.size();
// 随机版本
int i = rand() % (right - left + 1) + left;
swap(nums[left], nums[i]);
int l = left, r = right, mark = nums[left];
while(l < r){
while(l < r && nums[r] >= mark) r--;
while(l < r && nums[l] <= mark) l++;
swap(nums[l], nums[r]);
}
swap(nums[left], nums[l]);
if(right - l + 1 < k) return findKthLargestQuick(nums, k - (right - l + 1), left, l - 1);
else if(right - l + 1 == k) return nums[l];
else return findKthLargestQuick(nums, k , l + 1, right);
}
5 堆排序
1. 堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
2. 算法思想
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
3. 算法流程(以大顶堆为例)
- 堆调整maxHeapify
void maxHeapify(vector<int>& nums, int i, int heapSize){
int l = i * 2 + 1, r = i * 2 + 2, largest = i;
// 找到最大的一个元素
if(l < heapSize && nums[l] > nums[i]) largest = l;
if(r < heapSize && nums[r] > nums[largest]) largest = r;
// nums[i]符合堆要求,然后递归调整堆
if(largest != i){
swap(nums[largest], nums[i]);
maxHeapify(nums, largest, heapSize);
}
}
- 建堆
举例
// nums(n/2+1..n)都是叶节点,可以看做已经满足了堆的性质,无需调整
void buildMaxHeap(vector<int>& nums, int heapSize){
for(int i = heapSize / 2; i >= 0; i--){
maxHeapify(nums, i, heapSize);
}
}
- 堆排序
4. 题目举例
LeetCode 215.数组中的第k个最大元素
buildMaxHeap 初始构建堆
maxHeapfy 堆调整
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 一、快排
// return findKthLargestHelp(nums, k, 0, nums.size() - 1);
// 二、堆排
// 1. 构建size = nums.size()的大根堆,删除size - k + 1次,最后一个删除的
// 2. 构建size = k的小根堆,构建n次,最后一个堆的根
// return findKthLargestMaxHeap(nums, k);
return findKthLargestMinHeap(nums, k);
}
// quickSort 快排
int findKthLargestQuick(vector<int>& nums, int k, int left, int right){
// 快排思想
int n = nums.size();
int i = rand() % (right - left + 1) + left;
swap(nums[left], nums[i]);
int l = left, r = right, mark = nums[left];
while(l < r){
while(l < r && nums[r] >= mark) r--;
while(l < r && nums[l] <= mark) l++;
swap(nums[l], nums[r]);
}
swap(nums[left], nums[l]);
if(right - l + 1 < k) return findKthLargestQuick(nums, k - (right - l + 1), left, l - 1);
else if(right - l + 1 == k) return nums[l];
else return findKthLargestQuick(nums, k , l + 1, right);
}
// maxheap 大顶堆解决
int findKthLargestMaxHeap(vector<int>& nums, int k){
int heapSize = nums.size();
buildMaxHeap(nums, heapSize);
for(int i = nums.size() - 1; i >= nums.size() - k + 1; i--){
swap(nums[i], nums[0]);
--heapSize;
maxHeapify(nums, 0, heapSize);
}
return nums[0];
}
// 构建大顶堆
// nums(n/2+1..n)都是叶节点,可以看做已经满足了堆的性质,无需调整
void buildMaxHeap(vector<int>& nums, int heapSize){
for(int i = heapSize / 2; i >= 0; i--){
maxHeapify(nums, i, heapSize);
}
}
// 堆调整
void maxHeapify(vector<int>& nums, int i, int heapSize){
int l = i * 2 + 1, r = i * 2 + 2, largest = i;
// 找到最大的一个元素
if(l < heapSize && nums[l] > nums[i]) largest = l;
if(r < heapSize && nums[r] > nums[largest]) largest = r;
// nums[i]符合堆要求,然后递归调整堆
if(largest != i){
swap(nums[largest], nums[i]);
maxHeapify(nums, largest, heapSize);
}
}
// minHeap 小顶堆解决
int findKthLargestMinHeap(vector<int>& nums, int k){
int heapSize = k;
// 构建k大小的小根堆
for(int i = heapSize / 2; i >= 0; i--){
minHeapify(nums, i, heapSize);
}
for(int i = k; i < nums.size(); i++){
if(nums[0] < nums[i]){
swap(nums[0], nums[i]);
minHeapify(nums, 0, heapSize);
}
}
return nums[0];
}
void minHeapify(vector<int>& nums, int i, int heapSize){
int l = i * 2 + 1, r = i * 2 + 2, smallest = i;
if(l < heapSize && nums[l] < nums[i]) smallest = l;
if(r < heapSize && nums[r] < nums[smallest]) smallest = r;
if(smallest != i){
swap(nums[smallest], nums[i]);
minHeapify(nums, smallest, heapSize);
}
}
};
6 归并排序
1. 基本概念
**归并排序(MERGE-SORT)**是建立在归并操作上的一种有效的排序算法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之),将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序,若将两个有序表合并成一个有序表,称为二路归并。
归并排序其实要做两件事:
- (1)“分解”——将序列每次折半划分(递归实现)
- (2)“合并”——将划分后的序列段两两合并后排序
2. 基本思想
将待排序序列R[0…n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列
3.算法描述
- 第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾
4. 图解
5. 代码实现
/* 将序列对半拆分直到序列长度为1*/
void MergeSort_UptoDown(int *num, int start, int end){
int mid = start + (end - start) / 2;
if (start >= end) return;
MergeSort_UptoDown(num, start, mid);
MergeSort_UptoDown(num, mid + 1, end);
Merge(num, start, mid, end);
}
void Merge(int *num, int start, int mid, int end){
int *temp = (int *)malloc((end-start+1) * sizeof(int)); //申请空间来存放两个有序区归并后的临时区域
int i = start;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= end) {
if (num[i] <= num[j]) temp[k++] = num[i++];
else temp[k++] = num[j++];
}
while (i <= mid)
temp[k++] = num[i++];
while (j <= end)
temp[k++] = num[j++];
//将临时区域中排序后的元素,整合到原数组中
for (i = 0; i < k; i++)
num[start + i] = temp[i];
free(temp);
}
7 总结
本图片来源:知乎 @sugarTang