一、时间复杂度为O(n*logn)的排序算法
二、今日刷题
一、时间复杂度为O(n*longn)的排序算法
1、归并排序:排序的思想:就是不断分成两部分:左边和右边,分别将左边和右边有序之后才进行合并得到最后的有序的数组。
时间复杂度:o(n*logn)。
核心:归并排序之所以能达到o(n*logn)的时间复杂度,主要原因是其避免了很多不必要的排序,当保持左边和右边部分有序之后,其进行合并的时候进行比较的是不同组的元素之间的比较,组内的元素不再进行比较。
代码:
void merge(int a[], int l, int mid, int r)
{
int* help = new int[r - l + 1];//辅助数组
int p1 = l;
int p2 = mid + 1;
int k = 0;
while (p1 <= mid&&p2 <= r)
{
if (a[p1] <= a[p2])
{
help[k++] = a[p1];
p1++;
}
else
{
help[k++] = a[p2];
p2++;
}
}
while (p1 <= mid)
help[k++] = a[p1++];
while (p2 <= r)
help[k++] = a[p2++];
int len = r - l + 1;
for (int i = 0; i < len; ++i)
a[l + i] = help[i];
delete[] help;
//cout << "hello" << endl;
}
void mergesort(int a[], int l, int r)
{
if (l == r)
return;
int mid = l + ((r - l) >> 1);
mergesort(a, l, mid);
mergesort(a, mid + 1, r);
merge(a, l, mid, r);
}
例题:最小和问题和求逆序对,以最小和问题为例进行分析:
最小和问题:最小和也就是每个数的左边比其小的数的和,再将数组中所有的该和进行累加的结果,例如数组{3,5,2,6},a[0]左边没有比起小的,a[1]左边比其小的为3,a[2]左边比其小的没有,a[3]左边比其小的有3,5,2,所以数组的最小和为0+3+0+3+5+2.
分析:求最小和可以转化为求一个数的右边有多少个比其大的数,有几个那么其就贡献了几次最小和。求一个数右边有多少个比其大的使用归并排序的思想。在归并排序的merge的时候进行求解,核心思想是归并后的部分内部不必进行计算,只有不同部分之间才涉及计数;当出现相等的值的时候,先防止右边的数。
代码:
int merge(int a[], int l, int mid, int r)
{
int* help = new int[r - l + 1];//辅助数组
int p1 = l;
int p2 = mid + 1;
int k = 0;
int res = 0;
while (p1 <= mid&&p2 <= r)
{
if (a[p1] < a[p2])//左边的数字小,参与贡献
{
help[k++] = a[p1];
res += (r - p2 + 1)*a[p1];
p1++;
}
else//左边的数字大,那么就不参与贡献
{
help[k++] = a[p2];
p2++;
}
}
while (p1 <= mid)
help[k++] = a[p1++];
while (p2 <= r)
help[k++] = a[p2++];
int len = r - l + 1;
for (int i = 0; i < len; ++i)
a[l + i] = help[i];
delete[] help;
return res;
//cout << "hello" << endl;
}
int mergesort(int a[], int l, int r)
{
if (l == r)//递归结束的条件,注意不要忽略
return 0;
int mid = l + ((r - l) >> 1);
return mergesort(a, l, mid)+mergesort(a, mid + 1, r)+merge(a, l, mid, r);
//这行代码的理解个人觉得很重要
//首先将可以直接将左边家上右边再加上总体合并的,可以直接将merge(a,l,mir,r)相加的原因是
//同一个部分内部已经求解过了,也就是mergesort的部分,那么现在值涉及不同部分之间,所以直
//接加上不同部分合并的结果即可
}
2、堆排序(以大根堆为例)
堆结构其实就是一个数组,只不过将该数组视为一棵完全二叉树,对于数组中下标为i的节点,其左孩子的下标为2i+1,右孩子的下标为2i+2(数组的下标从0开始),父亲节点的下标为(i-1)/2。
堆结构中重要的操作有两个:heapinsert和headpify。heapinsert是指我们在堆中插入一个新的数字的时候,也就是在数组的末尾新增一个数字的时候将数据变成堆。
heapify是将堆顶元素删除,将剩下的数据重新变为堆。
小技巧:由于数组的长度和数据的数量一般是固定的,所以我们可以使用一个单独的变量heapsize来指定堆的大小。
代码:
void heapinsert(int a[], int index)//在数组的index位置上新增加一个数
{
while (a[index] > a[(index - 1) / 2])//这里已经考虑了边界条件,如果是到0,那么0和0自己比较就会跳出循环
{
int tmp = a[index];
a[index] = a[(index - 1) / 2];
a[(index - 1) / 2] = tmp;
index = (index - 1) / 2;
}
}
void heapify(int a[], int index,int size)
{
int left = index * 2 + 1;
int largest;
//在进行操作之前先判断是否有左孩子和右孩子
while (left < size)
{
int largest = left + 1 < size&&a[left + 1] > a[left]?left + 1 : left;
largest = a[largest]>a[index] ? largest : index;
if (largest == index)
break;
int tmp = a[largest];
a[largest] = a[index];
a[index] = tmp;
index = largest;
left = index * 2 + 1;
}
}
堆排序:有了上述的操作之后,堆排序就简单了,我们使用heapinsert逐个将数组的元素加入堆,也就是不断扩大对的大小,将数组符合最大堆的情况。然后每次将堆顶取出,之后用最后的元素代替该元素,然后在heapify将剩下的又调整成最大堆,每次都得到最大值,所以最后得到的就是从大到小排好序的。
进行数组的更新:如果要进行数组的更新,那么先看看能不能heapinsert,之后看能不能headpify。
由给定数组建堆:
方法一:时间复杂度为o(nlogn),这对数组中的数据是一个一个给的或者是一次性给的都适用,具体的方法就是对每一个数组中的数字从头到尾进行heapinsert。
方法二:时间复杂度为o(logn),这只针对的是数组是一次性给定的,我们先假定给定的数组就是一个大根堆,然后对堆中的每一个位置进行heapify,也就是判断自己与其子树是否构成大根堆,那么最底层有n/2个结点,其向下的层数就是其本身,倒数第二层有n/4,其操作2层……那么总的就是n/2+n/42+n/8*3……,最后求的复杂度为o(n)。
函数类库提供的堆:在C++中我们可以使用优先级队列,其就是一个堆:
//默认的就是大根堆
priority_queue<int> a;
//小根堆要指明比较其
priority_queue<int,vector<int>,greater<int> > b;
例题:
题目:已知一个几乎有序的数组,几乎有序是指如果把数组排好顺序的话,每个元素移动的距离不超过k,并且k相对于数组来说比较小。请选择一个适合的排序算法针对这个数据进行排序。
思路:使用小根堆(大根堆也是可以的),首先将0到k的k+1个数进入小根堆,由于距离不超过k,所以到0位置上的数一定在这个k+1个数之间,然后在对1到k+2上的数进小根堆得到的堆顶就是1位置上的数,依次就可以得到最后有序的数组。
时间复杂度:每次调整堆的时间复杂度为o(logK),一共有n个数,所以总的时间复杂度为o(n*logK)。
代码:
void sortedArrayDistanceLessK(vector<int> a, int k)
{
priority_queue<int, vector<int>, greater<int> > heap;
int len = a.size();
//先将0到k-1的数组进堆
int index = 0;
for (; index < min(len, k); index++)
heap.push(a[index]);
int i = 0;
for (; index < len; i++, index++)
{
heap.push(a[index]);
a[i] = heap.top();
heap.pop();
}
while (!heap.empty())
{
a[i++] = heap.top();
heap.pop();
}
}
3、快排之partition:
快排的核心思想就是partition,而所谓partition也就是给定一个数组和一个值,将数组小于给定值的放在数组的左边,大于该值的放在数组的右边。要求时间复杂度为o(n),空间复杂度为o(1)。
思路:设置一个小于等于区域,对于数组的当前的数,如果其小于等于给定的数那么其就与小于等于区域的下一个数交换,否则直接跳下一个数。从数的位置可以直观理解:数组中的数:小于等于区域---->大于区域----->当前的值。
例题:给定数组和一个值,将小于该值的放在数组的左边,等于该值的放在数组的中间,大于该值的放在数组的右边。
思路:仍然指定小于区域,等于区域和大于区域;当前的值如果小于给定的值,那么就让其与小于区域的下一个数交换,并且将当前值前进一个数;如果当前值等于给定值直接将当前值前进一个数;如果大于将当前值与大于区域的前一个值交换,当前的位置不变。从数组的位置直观理解就是:小于区域的值------>等于区域的值------->当前值------->大于区域的值。
代码:
void partition(int a[], int l, int r,int label)
{
int less = l - 1;
int more = r + 1;
int tmp;
while (l < more)//当当前的位置和大于区域相遇的时候结束
{
if (a[l] < label)
{
tmp = a[l];
less++;
a[l] = a[less];
a[less] = tmp;
l++;
}
else if (a[l]>label)
{
tmp = a[l];
more--;
a[l] = a[more];
a[more] = tmp;
//当前位置不变
}
else
l++;
}
}
二、今日刷题
题目:用两个栈实现一个队列的功能思路:队列和栈之间的区别在于,队列是先进先出,栈是后进先出。这里两个栈为s1和s2,实现队列的出队列功能:s1如果有元素的话,就将s1的元素压倒s2中,然后弹出s2栈顶的元素,如果s1是空s2有元素,那么直接弹出s2栈顶的元素;如果两个都是空的,那么报错。实现入队列,如果s2中有元素,先将s2中的元素入到s1中,然后再将新的元素push进s1中,否则直接push进s1,代码为:
class Solution
{
//push的时候如果s2是空的,那么直接push进s1,如果s2非空,那么就要先将s2中的元素push如s1
//pop的时候,如果s2非空,那么直接pop,如果s2为空,那么将s1中的push如s2,然后再pop
public:
void push(int node) {
int tmp;
if(!s2.empty())
{
tmp = s2.top();
s2.pop();
s1.push(tmp);
}
s1.push(node);
}
int pop() {
int tmp;
if(!s2.empty())
{
tmp = s2.top();
s2.pop();
return tmp;
}
else if(s1.empty())
{
cout<<"erroe!";
return -1;
}
else
{
while(!s1.empty())
{
tmp = s1.top();
s1.pop();
s2.push(tmp);
}
tmp = s2.top();
s2.pop();
return tmp;
}
}
private:
stack<int> s1;
stack<int> s2;
};