Reference
一、快排
1.1 荷兰国旗问题引入
分析
1.把数组划分为小于等于num的区域和大于num的区域。用变量x代表小于区域的位置。刚开始由于不存在小于等于的区域,则x = -1,表示指向数组-1(不存在)。再设置变量cur作为遍历的位置,刚开始cur = 0指向数组第一个数。
2.开始遍历
(1)如果cur<=num
,那么把cur当前的元素和小于等于区域的下一个数交换,即++x的位置和cur位置的数交换,cur,x后移;
(2)如果cur>num
,cur后移;
(3)如果cur == arr_length
结束遍历。
eg [4,6,7,3],num = 5
1.cur 指向4,4<5,把4和x的下一位即0号位置4(自己和自己)交换。之后x指向4,cur++,cur指向6;
2.cur指向6,5<6,x不变,cur++;
3.cur指向7,5<7,x不变,cur++;
4.cur指向3,3<5,交换,x的下一位即6和3交换。之后x指向3,cur++,退出循环。
此时,x指向的3以及之前的4为小于等于区域,而x之后的7,6为大于区域。
/******************************************************************************
* 问题一:荷兰国旗问题引入
* 给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。
* 要求额外空间复杂度O(1),时间复杂度O(N)
******************************************************************************/
/*
* @brief partition函数和快排中的partion一样
* @param[in] arr 输入数组
* @param[in] num 需要比较的值
* @return void
*/
void partition(vector<int> &arr, int num)
{
int x = -1; //代表小于区域的位置
int cur = 0; //变量cur作为遍历的位置
while (cur < arr.size()) {
if (num >= arr[cur]) {
swap(arr[cur++], arr[++x]);
} else { //num < arr[cur]
cur++;
}
}
}
1.2 荷兰国旗问题及复杂度分析
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
分析
和预备问题相似
1.把数组划分为小于num的区域、等于num的区域和大于num的区域。用变量less代表小于区域的位置、more代表大于num的区域。刚开始由于不存在小于和大于的区域,则less = l-1,more = r + 1,表示指向数组l-1、r + 1(不存在)。再设置变量cur作为遍历的位置,刚开始cur = l指向数组第一个数。
2.开始遍历
(1)如果cur<num ,那么把cur当前的元素和小于区域的下一个数交换,即++x的位置和cur位置的数交换,cur,x后移;
(2)如果cur>num,那么把cur当前的元素和大于区域的前一个数交换,即–y的位置和cur位置的数交换,y前移,cur不变(因为交换过来的数不确定)
(3)如果cur == num,cur++。
(4)如果cur == more结束。
实现
/******************************************************************************
* 问题二(荷兰国旗问题)
* 给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,
* 等于num的数放在数组的中间,大于num的数放在数组的右边。
* 要求额外空间复杂度O(1),时间复杂度O(N)
******************************************************************************/
/*
* @brief partition函数和快排中的partion一样
* @param[in] arr 输入数组
* @param[in] l 需要处理区间的最小值
* @param[in] r 需要处理区间的最大值
* @param[in] num 需要比较的值
* @return
*/
vector<int> partition(vector<int> &arr, int l, int r, int num)
{
int less = l - 1; //变量less代表小于区域的位置
int more = r + 1; //变量more代表大于num的区域
int cur = l; //变量cur作为遍历的位置
while (cur < more) {
if (num > arr[cur]) {
swap(arr[++less], arr[cur++]);
} else if (num < arr[cur]) {
swap(arr[cur], arr[--more]);
} else { // == num
cur++;
}
}
return vector<int> {less + 1, more - 1};
}
// for test
int main ()
{
vector<int> input{2,5,9,6,2,3,4,2,7,1,3,0,0};
vector<int> res;
res = partition(input,0,12,3);
for (int i = 0; i < input.size(); ++i) {
cout << input[i] << " ";
}
cout<< endl;
cout << res[0] << endl << res[1] <<endl;
}
1.3 排序算法–快排(经典快排,改进快排,随机快排)
- 首先在数组[l,r]中把最后一个元素作为num进行比较,得到两段区域,分别为小于等于最后一个数的区域和大于最后一个数的区域。
- 在上述第一段区域内,最后一个数的值为num保留不动,倒数第二个数重新命名为num,接着继续比较,不断迭代。
- 在上述1的第二段区域内,把最后一个数命名为num,接着继续比较,不断迭代。
- 迭代继续的条件是if(l<r)
/**
* brief 经典快速排序,思路和荷兰国旗问题中的预备题目一样:
* 1.首先在数组[l,r]中把最后一个元素作为num进行比较,得到两段区域,分别为小于等于最后一个数的区域和大于最后一个数的区域。
2.在上述第一段区域内,最后一个数的值为num保留不动,倒数第二个数重新命名为num,接着继续比较,不断迭代。
3.在上述1的第二段区域内,把最后一个数命名为num,接着继续比较,不断迭代。
4.迭代继续的条件是if(l<r)
* @param[in] arr 输入数组
* @param[in] l 需要处理区间的最小值
* @param[in] r 需要处理区间的最大值
* return 返回值为less:返回小于等于num的范围内最后一个元素的位置(最后一个位置就是原来的num),
* 下次直接用倒数第二个位置元素(小于等于num区间的倒数第二个元素)作为下一次的num。
*/
int partition(vector<int> arr, int l, int r )
{
int num = arr[r]; // 把最后一个数设为num进行比较
int less = l - 1; // less为小于等于的区域,设为区域之前一个位置
int cur = l; // cur是当前遍历的指针
while (cur < r + 1) { // 遍历所有位置
if (arr[cur] <= num) {
swap(arr[cur++], arr[++less]);
} else {
cur++;
}
return less;
}
}
// 迭代部分
void QuickSort(vector<int> arr, int l, int r)
{
if (l < r) { // 迭代的条件是在[l,r]范围内,如果越界表示不可继续分
int pivot = partition(arr, l, r);
QuickSort(arr, l, pivot - 1);
QuickSort(arr, pivot + 1, r);
}
}
/**
* brief: 荷兰国旗改进的经典快排
* 经典快排的缺点:每次只找出一个num进行排序,如果存在多个相同值的num还需要继续划分,多做了无用功。
* 考虑如果使用荷兰国旗的排序方法,将相同的num一次找出来,那么时间复杂度的常数时间就可以缩短。
* 荷兰国旗问题需要有两个指针记录小于num的范围和大于num的范围,考虑定义一个长度为2的数组记录下来,返回数组的指针用作下一步。
* @param[in] arr 输入数组
* @param[in] l 需要处理区间的最小值
* @param[in] r 需要处理区间的最大值
* @return 存放等于num值的索引最小值最大值的数组
*/
// 返回arr[r]在排序后的索引区间值
vector<int> partition(vector<int> arr, int l, int r)
{
int num = arr[r]; // 首先在数组[l,r]中把最后一个元素作为num进行比较
int less = l - 1; // less为小于等于num区间的之前的一个位置
int more = r + 1;
int cur = l;
while (cur < more) {
if (arr[cur] < num) {
swap(arr[++less], arr[cur++]);
} else if (arr[cur] > num) {
swap(arr[cur], arr[--more]); // 此时cur并没有+1,因为交换后arr[cur]与num的大小关系未知
} else {
cur++;
}
return vector<int> {less + 1, more - 1};
}
}
// 分治法迭代
void QuickSort(vector<int> arr, int l, int r)
{
if (l < r) {
vector<int> index = partition(arr, l, r); // index存储划分点
QuickSort(arr, l, index[0] - 1); // [l, res[0] - 1]左子区间
QuickSort(arr, index[1] + 1, r); // [res[1] + 1, r]右子区间
}
}
/**
* brief: 随机快排。
* 荷兰国旗改进的经典快排缺点在于使用最后一个数作为num,
* 例如[1,2,3,4,5,6]中复杂度就很高,因为此时复杂度就与数据状况有关了。
* 考虑:在数组中随机选一个数作为num,这样就可以绕开原始数据状况,复杂度变成长期的期望值。
*/
// 分治法迭代
void QuickSort(vector<int> arr, int l, int r)
{
if (l < r) {
swap(arr[l+rand()%(r-l+1)], arr[r]); // 随机快排
vector<int> index = partition(arr, l, r); // index存储划分点
QuickSort(arr, l, index[0] - 1); // [l, res[0] - 1]左子区间
QuickSort(arr, index[1] + 1, r); // [res[1] + 1, r]右子区间
}
}
/**
复杂度分析:
时间复杂度为度O(NlogN),额外空间复杂度O(logN)
1.对于长期选择来说,随机成中间的值可能性最大,分治思想T(n) = 2T(N/2) + O(n),时间复杂度为度O(NlogN)。
2.随机查找时,二分多少次多少个断点,最好情况O(logN),最差为O(N),概率上长期为O(logN)。
*/
/**
* brief 经典快速排序(二),基准数据找其正确索引位置的过程
* partition 函数找到分区后的第一个元素正确索引的位置
* @param[in] arr 输入数组
* @param[in] low 需要排序区间的下限值
* @param[in] high 需要排序区间的上限值
*/
// 找到arr[low]的正确索引
int partition2(vector<int> arr, int low, int high)
{
int pivot = arr[low]; // 设最开始的基准数据为数组第一个元素arr[low],则首先用一个临时变量去存储基准数据
while (low < high) {
while (low < high & arr[high] > pivot) { // 当队尾的元素大于等于基准数据时,向前挪动high指针
high--;
}
arr[low] = arr[high]; // 如果队尾元素小于基准数据了,需要将其赋值给low
while (low < high & arr[low] <= pivot) { // 当队首元素小于等于pivot时,向前挪动low指针
low++;
}
arr[high] = arr[low]; // 当队首元素大于pivot时,需要将其赋值给high
}
arr[low] = pivot; // 跳出循环时low和high相等,此时的low或high就是pivot的正确索引位置
return low; // 返回pivot的正确位置
}
void QuikSort(vector<int> arr, int low, int high)
{
if (low < high) {
int pivot = partition2(arr, low, high); // 找arr[low]正确索引的位置
QuikSort(arr, low, pivot); // 分治法递归左半部分
QuikSort(arr, pivot + 1, high); // 分治法递归右半部分
}
}
二、堆 & 堆排序
2.1 基础知识
1.完全二叉树有两种:
5
/
1 2
/ \ /
3 4 2 6
满二叉树
5
/
1 2
/
1
从左到右排序的二叉树
2.完全二叉树可等价为堆,堆可用数组实现
其中第i结点的左孩子:2i+1,第i结点的右孩子:2i+2,第i结点的父节点:(i-1)/2
3.大根堆:指任意子树的最大值是其头部的结点
小根堆:指任意子树的最小值是其头部的结点
2.2 heapInsert:新结点加入进来并向上调整为大根堆的过程
把数组变为大根堆,建立堆结构
分析
如果当前插入的元素大于其父节点的元素,那么交换父节点与当前节点的位置。接着考察更换结点后的插入元素与新位置的父节点大小,如果还是大于还要继续交换。
eg。例[2,1,3,6]变换为大根堆
位置0,1,2,3
- 2插入,父节点(0-1)/2 = 0位置,自己和自己不动;
- 1插入,父节点(1-1)/2 = 0位置,1<2,不动;
- 3插入,父节点(2-1)/2 = 0位置,3>2,交换位置;此时3的位置变为0,父节点(0-1)/2 = 0位置,自己和自己不动;
此时:
3
/
1 2
- 6插入,父节点(3-1)/2 = 1位置,6>1,交换位置;此时6的位置变为1,父节点(1-1)/2 = 0位置;6>3,继续交换;
6
/
3 2
/
1
核心代码
(index-1)/2
是index
父节点的位置,index是新插入节点在数组中的下标
void heapinsert(int arr[],int index)
{
while(arr[index] > arr[(index-1)/2])
{
swap(arr[index],arr[(index-1)/2]);
index = (index-1)/2;
}
}
使用for循环遍历数组,调用heapinsert
for(int i = 0;i < length;i++ )
{
heapinsert(arr,i); // 用for循环传入要处理的index
}
#include<iostream>
#define length 5
using namespace std;
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
void heapinsert(int arr[],int index)
{
while(arr[index] > arr[(index-1)/2])
{
swap(arr[index],arr[(index-1)/2]);
index = (index-1)/2;
}
}
int main()
{
//int arr[length] = {2,1,3,6,0,4};
int arr[length] = {3,4,5,1,2};
int heapsize = length;
for(int i = 0;i < heapsize;i++ )
{
heapinsert(arr,i);//用for循环传入要处理的index
}
system("pause");
return 0;
}
2.3 heapify:假设数组中一个值变小了,重新调整为大根堆的过程
分析
- 找出这个数的左右孩子中的最大值,将这个最大值与改变的值进行比较,如果改变的值大于左右孩子的最大值,则交换两个数,接着找到交换后的左右孩子,继续比较。
- 继续比较的条件:左孩子未越界。
核心代码
比较左右孩子和当前的数,找出最大值赋给largest;如果当前数是largest则跳出,如果不是,则交换两个数,更新index,和左孩子位置,继续比较。
/**
* brief 假设数组中一个值变小了,重新调整为大根堆的过程:
* 比较左右孩子和当前的数,找出最大值赋给largest;如果当前数是largest则跳出,
* 如果不是,则交换两个数,更新index,和左孩子位置,继续比较。
* @param[in] index 需要调整的元素的下标
* @param[in] heapsize [0, heapsize-1]区间为堆的区间,堆的长度一定小于等于arr.size()
* @return void
*/
void heapify(int arr[],int index,int heapsize)
{
int left = index * 2 + 1;
while (left < heapsize) { // 左孩子没有越界
int largest = left + 1 < heapsize && arr[left]<arr[left + 1] ? // 有右孩子(右孩子没有越界)&&
left + 1:left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) { // 虽然变了,但依然满足最大堆,直接退出
break;
}
swap(arr[largest],arr[index]);
index = largest;
left = index * 2 + 1;
}
}
2.4 堆排序
HeapInsert结合Heapify。
(1)让数组变为大根堆(heapinsert);
(2)让最后一位和堆顶交换,堆大小减一(保存最大值)heapsize–;
(3)再重新变为大根堆,相当于把堆顶元素变小再重排(heapify)
(4)直到堆大小为1停止。
/************************************************************************************
* 堆排序
************************************************************************************/
/**
* brief 假设数组中一个值变小了,重新调整为大根堆的过程:
* 比较左右孩子和当前的数,找出最大值赋给largest;如果当前数是largest则跳出,
* 如果不是,则交换两个数,更新index,和左孩子位置,继续比较。
* @param[in] index 需要调整的元素的下标
* @param[in] heapsize [0, heapsize-1]区间为堆的区间,堆的长度一定小于等于arr.size()
* @return void
*/
void Heapify(vector<int> arr,int index,int heapsize)
{
int left = index * 2 + 1;
while (left < heapsize) { // 左孩子没有越界
int largest = left + 1 < heapsize && arr[left]<arr[left + 1] ? // 有右孩子(右孩子没有越界)&&
left + 1:left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) { // 虽然变了,但依然满足最大堆,直接退出
break;
}
swap(arr[largest],arr[index]);
index = largest;
left = index * 2 + 1;
}
}
// 建初始堆:新结点加入进来并向上调整为大根堆的过程
void HeapInsert(vector<int> arr,int index)
{
while(arr[index] > arr[(index-1)/2]) {
swap(arr[index],arr[(index-1)/2]);
index = (index-1)/2;
}
}
/*
* 堆排序流程:
* (1)建立初始堆;
* (2)将堆顶元素与堆中最后一个元素交换,剔除最后一个元素(HeapSize减少1),调整新堆
*/
void HeapSort(vector<int> arr)
{
if (arr.empty() || arr.size() < 2) {
return;
}
for (int i = 0; i < arr.size(); ++i) {
HeapInsert(arr, i);
}
int len = arr.size();
swap(arr[0], arr[--len]);
while (len > 0) {
Heapify(arr, 0, len);
swap(arr[0], arr[--len]);
}
}