算法学习(5):O(NlogN)的排序
O(NlogN)的排序
master公式
T(N) = aT(N/b) + O(N^d)
- b:子过程的样本量
- a:子过程的计算次数
- O(N^d):除了递归之外操作的时间复杂度
满足如上公式的程序都可以根据master公式计算时间复杂度:
- log(b,a) > d :时间复杂度为O(N^log(b,a))
- log(b,a) = d :时间复杂度为O(N^d * logN)
- log(b,a) < d :时间复杂度为O(N^d)
归并排序
归并排序的基本思想
归并排序的整体就是一个递归的过程,左边排好序,右边排好序,再让其整体有序
归并排序的C++代码实现
void process(vector<int>& arr, int left, int right);
void merge(vector<int>& arr, int left, int mid, int right);
void mergeSort(vector<int> &arr)
{
if (arr.size() < 2)
{
return;
}
process(arr, 0, arr.size() - 1);
}
void process(vector<int>& arr, int left, int right)
{
if (left == right)
return;
int mid = left + (right - left) / 2;
process(arr, left, mid);
process(arr, mid + 1, right);
merge(arr, left, mid, right);
}
void merge(vector<int>& arr, int left, int mid, int right)
{
vector<int> help;
int p1 = left;
int p2 = mid + 1;
while(p1 <= mid && p2 <= right)
if (arr[p1] <= arr[p2])
help.push_back(arr[p1++]);
else
help.push_back(arr[p2++]);
while (p1 <= mid)
help.push_back(arr[p1++]);
while (p2 <= right)
help.push_back(arr[p2++]);
for (int i = 0; i < help.size(); i++)
arr[left + i] = help[i];
}
利用master公式求解归并排序的时间复杂度
T(N) = 2 * T(N/2) + O(N)
- a = 2,因为process函数里调用了两次自身
- b = 2,因为每次分二分之一
- d = 1,因为merge函数的时间复杂度为O(N)
log(2, 2) = 1 = d,所以归并排序的时间复杂度是O(N*logN)
归并排序的额外空间复杂度
O(N),可以最多准备长度为N的额外空间给merge的过程重复使用,每次使用完释放。
归并排序算法题
(1)小和问题:在一个数组中,每一个数左边比当前数小的数累加起来名叫作这个数组的小和。求一个数组的小和。
例子:[1,3,4,2,5]
- 1左边比1小的数,没有;
- 3左边比3小的数,1;
- 4左边比四小的数,1、3;
- 2左边比2小的数,1;
- 5左边比5小的数,1、3、4、2;
- 所以小和为1+1+3+1+1+3+4+2=16
解题思路:将小和的求法由左边比当前数小转化为右边比当前数大,右边有几个比自身大的数,就有几个自身的小和。
例如:[1,3,4,2,5]
- 1右边比1大的数,3、4、2、5,四个,所以有4个1的小和;
- 3右边比3大的数,4、5,两个,所以有2个3的小和;
- 4右边比四大的数,5,一个,所以有1个4的小和;
- 2右边比2大的数,5,一个,所以有1个2的小和;
- 5右边比5大的数,没有;
- 所以小和为1+1+1+1+3+3+4+2=16。
然后利用归并排序的思想,从中间分割一直往下,左边最下层1、3,左边的数与右边的数依次比较,如果右边有比左边的数大的,就累加一个左边小的数,依次往上回溯,通俗来讲就是比归并排序在merge函数中多了个累加小和的过程,并且归并排序的merge函数默认左右两边相等时拷贝左边的数字,但是小和问题中要拷贝右边的数字,仔细解释起来比较麻烦,详细算法思想见https://www.bilibili.com/video/BV13g41157hK?p=4&vd_source=77d06bb648c4cce91c6939baa0595bcd P4 1:01:37开始。
C++代码实现:
int ssProcess(vector<int>& arr, int left, int right);
int ssMerge(vector<int>& arr, int left, int mid, int right);
int smallSum(vector<int>& arr)
{
if (arr.size() < 2)
{
return 0;
}
return ssProcess(arr, 0, arr.size() - 1);
}
int ssProcess(vector<int>& arr, int left, int right)
{
if (left == right)
return 0;
int mid = left + (right - left) / 2;
return ssProcess(arr, left, mid) + ssProcess(arr, mid + 1, right) + ssMerge(arr, left, mid, right);
}
int ssMerge(vector<int>& arr, int left, int mid, int right)
{
vector<int> help;
int p1 = left;
int p2 = mid + 1;
int res = 0;
while (p1 <= mid && p2 <= right)
{
if (arr[p1] < arr[p2])
{
res += (right - p2 + 1) * arr[p1];
help.push_back(arr[p1++]);
}
else
help.push_back(arr[p2++]);
}
while (p1 <= mid)
help.push_back(arr[p1++]);
while (p2 <= right)
help.push_back(arr[p2++]);
for (int i = 0; i < help.size(); i++)
arr[left + i] = help[i];
return res;
}
(2)逆序对问题:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
解题思路:与上面小和问题的大致思路一样,小和问题求的是比右边的数小的情况,这里是反过来了。注意一点,这里排序需要从大到小排。
C++代码实现:
void ROPProcess(vector<int>& arr, int left, int right);
void ROPMerge(vector<int>& arr, int left, int mid, int right);
void reverseOrderPair(vector<int>& arr)
{
if (arr.size() < 2)
{
return;
}
ROPProcess(arr, 0, arr.size() - 1);
}
void ROPProcess(vector<int>& arr, int left, int right)
{
if (left == right)
return;
int mid = left + (right - left) / 2;
ROPProcess(arr, left, mid);
ROPProcess(arr, mid + 1, right);
ROPMerge(arr, left, mid, right);
}
void ROPMerge(vector<int>& arr, int left, int mid, int right)
{
vector<int> help;
int p1 = left;
int p2 = mid + 1;
while (p1 <= mid && p2 <= right)
{
if (arr[p1] > arr[p2])
{
cout << "(" << arr[p1] << ", " << arr[p2] << ")" << " ";
help.push_back(arr[p1++]);
}
else
help.push_back(arr[p2++]);
}
while (p1 <= mid)
help.push_back(arr[p1++]);
while (p2 <= right)
help.push_back(arr[p2++]);
for (int i = 0; i < help.size(); i++)
arr[left + i] = help[i];
}
快速排序
荷兰国旗问题
(1)给定一个数组arr和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。
解题思路:双指针法,一个指针i指向当前数,另一个指针L指向排好的小于等于num的数的最后一个。
i移动:
- arr[i] <= num时,arr[i]与L指针指向的数字的下一个数即arr[L+1]交换,L++,i++;
- arr[i] > num时,i++。
当i越界时停止。
详细算法思路解释见https://www.bilibili.com/video/BV13g41157hKp=4&vd_source=77d06bb648c4cce91c6939baa0595bcd P4 1:45:40 开始
C++代码实现:
void doubleColor(vector<int>& arr, int num)
{
int i = 0;
int L = -1;
for (; i < arr.size(); i++)
{
if (arr[i] <= num)
{
swap(arr, i, L + 1); //交换数组arr中下标为i和L+1的数,往期文章中提到过,这里不再作实现
L++;
}
}
}
(2)(荷兰国旗问题)给定一个数组arr和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。
解题思路:与上一题的思路类似,三指针法,一个指针i指向当前数,一个指针L指向排好的小于num的数的最后一个,另一个指针R指向排好的大于num的数的最前面一个。
i移动:
- arr[i] < num时,arr[i]与L指针指向的数字的下一个数即arr[L+1]交换,L++,i++;
- arr[i] = num时,i++;
- arr[i] > num时,arr[i]与R指针指向的数字的前一个数即arr[R-1]交换,R--,i不变。
当i等于R时停止。
详细算法思路解释见https://www.bilibili.com/video/BV13g41157hKp=4&vd_source=77d06bb648c4cce91c6939baa0595bcd P4 1:53:50
C++代码实现:
void tripleColor(vector<int>& arr, int num)
{
int i = 0;
int L = -1;
int R = arr.size();
while (i < R)
{
if (arr[i] < num)
{
swap(arr, i, L + 1);
L++;
i++;
}
else if (arr[i] == num)
i++;
else
{
swap(arr, i, R - 1);
R--;
}
}
}
快速排序1.0版本
思路:荷兰国旗第一问思路,将给定的num改为数组最后一个数字,小于等于num的放在数组左边,大于num的放在数组右边,然后将数组分为两部分,左边部分从下标为0的数开始,到小于等于num的最后一个数结束。右边部分从大于num的第一个数开始,到数组的最后一个数结束。然后对这两部分数组递归,重复前面的操作。
快速排序2.0版本
思路:荷兰国旗第二问思路,将给定的num改为数组最后一个数字,小于num的放在数组左边,等于num的放在数组中间,大于num的放在数组右边,然后将数组分为三部分,左边部分从下标为0的数开始,到小于num的最后一个数结束。中间部分为等于num的数。右边部分从大于num的第一个数开始,到数组的最后一个数结束。然后对这左右两部分数组递归,中间部分不动,重复前面的操作。
由于2.0版本中间不用动的是等于num的数,每次递归搞定一这批数,所以比1.0版本要好。但是由于选择的数字不知道大小,最好的情况是选到的数字刚好是数组的最中间的数,递归的两边长度相等,这时利用master公式算出的时间复杂度是O(NlogN);最坏的情况是数组已经排好序,最后一个数是最大的数,每次递归左半部分为当前数组长度-1,右半部分为0,利用master公式算出的时间复杂度就是 O(N2),由于算法的时间复杂度是最差的情况,所以整个算法的时间复杂度就是O(N2)。这种时间复杂度太高了,于是快速排序3.0版本应运而生
快速排序3.0版本
在快速排序2.0版本的基础上,只改动了一点:从原来的从数组最后一个数取num改为从数组中随机取num,这样,每次取得的数大小是随机的,有时好有时坏,这种情况利用master公式算出时间复杂度的长期期望是O(NlogN),即整个算法的时间复杂度是O(NlogN)。
快速排序3.0版本C++代码实现
void QSProcess(vector<int>& arr, int left, int right);
vector<int> partition(vector<int>& arr, int left, int right);
void queckSort(vector<int> &arr)
{
if (arr.size() < 2)
{
return;
}
QSProcess(arr, 0, arr.size() - 1);
}
void QSProcess(vector<int>& arr, int left, int right)
{
if (left < right)
{
srand((unsigned)time(NULL));
int pos = (rand() % (right - left + 1)) + left; //随机选择一个数组中的下标
swap(arr, pos, right); //将选中的下标与数组中的最后一个数交换
vector<int> p = partition(arr, left, right);
QSProcess(arr, left, p[0] - 1);
QSProcess(arr, p[1] + 1, right);
}
}
vector<int> partition(vector<int>& arr, int left, int right)
{
int i = left;
int p1 = left - 1;
int p2 = right + 1;
int num = arr[right];
while (i < p2)
{
if (arr[i] < num)
{
swap(arr, i++, ++p1);
}
else if(arr[i] > num)
{
swap(arr, i, --p2);
}
else
{
i++;
}
}
return { p1 + 1,p2 - 1 };
}
快速排序的额外空间复杂度
与时间复杂度类似,概率累加是O(logN)。
堆排序
大根堆,小根堆
大根堆:所有的父亲节点都比孩子节点大
小根堆:所有的父亲节点都比孩子节点小
堆排序算法思想
首先堆的大小heapSize定义为数组大小,然后将heapSize代表的堆变为大根堆,之后将堆的第一个数(最大的数)与最后一个数交换位置,heapSize减一,然后重复前面的操作,周而复始。让堆变为大根堆的操作heapInsert和heapIfy的算法思想见https://www.bilibili.com/video/BV13g41157hKp=5&vd_source=77d06bb648c4cce91c6939baa0595bcd P5 0:18:48
堆排序C++代码实现
void heapInsert(vector<int>& arr, int index);
void heapIfy(vector<int>& arr, int index, int heapSize);
void heapSort(vector<int>& arr)
{
if (arr.size() < 2)
{
return;
}
for (int i = 0; i < arr.size(); i++)
{
heapInsert(arr, i);
}
int heapSize = arr.size();
swap(arr, 0, --heapSize);
while (heapSize > 0)
{
heapIfy(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
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, int index, int heapSize)
{
int left = 2 * index + 1;
while (left < heapSize)
{
int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
largest = arr[index] > arr[largest] ? index : largest;
if (largest == index)
break;
swap(arr, index, largest);
index = largest;
left = 2 * index + 1;
}
}
浙公网安备 33010602011771号