归并排序和快速排序的衍生问题

前面两篇总结了常见的几种排序算法的主要思想以及C++与python两种方式的实现过程, 几种排序算法中比较重要的就是归并排序和快速排序,这两种方法的相同点就是都使用了分治的思想,现在用来解决两个具体问题。

1.分治法

  分治法就是将原问题分割成同等结构的子问题,之后将子问题逐一解决后,原问题也就得到了解决。 需要注意的是归并排序和快速排序虽然都使用了分治的思想,但它们分别代表了分治算法的两类基本思想。对于归并排序而言,它对“分”这个过程没有做太多操作,只是简单的将数组分为两部分然后递归的进行归并排序,而归并排序的关键是这样分完之后如何将它们归并起来,即merge()操作。 
  而对于快速排序来说,则是废了很大功夫放在了如何“分”这个问题上,我们是选取了一个标定点,然后使用partition()这个子过程将这个标定点移到了合适的位置,当它移到了合适的位置之后才将整个数组分成了两部分,而这样分完之后,在“合”的时候就不用做过多的考虑了,只需要一步一步递归下去就好了。 
  下面解决两个直接从归并排序和快速排序中衍生出来的具体问题的。

2.求逆序对数量

  • 问题描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。给定一个数组,求出这个数组中的逆序对的总数。
  • 这个问题直接的做法是逐个统计,复杂度是O(n^2),但显然是很愚蠢的方法,这里可以利用归并排序的思想,在归并排序过程中统计逆序对的个数。
  • 实现思想: 
      归并排序是将数列a[l,r]分成两部分,a[l,mid]和a[mid+1,r]分别进行归并排序,然后再将这两半合并起来。在合并的过程中(设l<=i<=mid,mid+1<=j<=r),当a[i]<=a[j]时,并不产生逆序数;当a[i]>a[j]时,在前半部分中比a[i]大的数都比a[j]大,将a[j]放在a[i]前面的话,逆序数要加上mid+1-i。因此,可以在归并排序中的合并过程中计算逆序数。
  • C++实现:
#include <iostream>
using namespace std;
// 计算逆序数对的结果以long long返回
// 因为对于一个大小为n的数组, 其最大的逆序数对个数为 n*(n-1)/2, 非常容易产生整型溢出
// __merge函数求出在arr[l,mid]和arr[mid+1,r]有序的基础上, arr[l,r]的逆序数对个数
long long __merge(int arr[],int l,int mid,int r){
long long res = 0;// 初始化逆序数对个数 res = 0
int aux[r - l + 1];
for(int i = l;i <= r;i++)
aux[i - l] = arr[i];
int i = l;
int j = mid + 1;
for(int k = l;k <= r;k++){
if(i > mid){
arr[k] = aux[j - l];
j ++;
}
else if(j > r){
arr[k] = aux[i - l];
i ++;
}
else if(aux[i - l] < aux[j - l]){
arr[k] = aux[i - l];
i ++;
}
else{// 左半部分所指元素 > 右半部分所指元素
arr[k] = aux[j - l];
j ++;
res += (long long) (mid - i + 1);
// 此时, 因为右半部分所指的元素小
// 这个元素和左半部分的所有未处理的元素都构成了逆序数对
// 左半部分此时未处理的元素个数为 mid - j + 1
}
}
return res;
}
// 求arr[l,r]范围的逆序数对个数
long long __inversionCount(int arr[],int l,int r){
if(l >= r)
return 0;
int mid = l + (r - l) / 2;
long long res1 = __inversionCount(arr,l,mid);// 求出 arr[l,mid] 范围的逆序数
long long res2 = __inversionCount(arr,mid + 1,r);// 求出 arr[mid+1,r] 范围的逆序数
return res1 + res2 + __merge(arr,l,mid,r);
}
long long inversionCount(int arr[],int n){
return __inversionCount(arr,0,n - 1);
}
int main(){
int a[10] = {10,9,8,7,6,5,4,3,2,1};
int n = 10;
cout<< "逆序对数量为:" << inversionCount(a,n)<<endl;
}
  • Python实现:
 
def __merge(arr,l,mid,r):
res = 0
aux = []
for x in range(l,r + 1):
aux.append(arr[x])
i = l
j = mid + 1
for k in range(l,r + 1):
if i > mid:
arr[k] = aux[j - l]
j += 1
elif j > r:
arr[k] = aux[i - l]
i += 1
elif aux[i - l] < aux[j - l]:
arr[k] = aux[i - l]
i += 1
else:
arr[k] = aux[j - l]
j += 1
res += mid - i + 1
return res
def __inversionCount(arr,l,r):
if l >= r:
return 0
mid = l + (r - l) // 2
res1 = __inversionCount(arr,l,mid)
res2 = __inversionCount(arr,mid + 1,r)
return res1 + res2 + __merge(arr,l,mid,r)
def inversionCount(arr):
n = len(arr)
return __inversionCount(arr,0,n - 1)
arr = [10,9,8,7,6,5,4,3,2,1]
res = inversionCount(arr)
print('逆序对数量为:' + str(res))

 

3.取数组中第k大的元素

  • 题目很简单,给定一个数组,求出该数组中第k大的元素。最简单的做法是直接进行排序,算法复杂度是O(nlogn),但是这么做很明显比较低效率,因为只要求出第k大的元素,并不需要别的信息。
  • 另一种方法是用快速排序的思想。快速排序每次把一个元素交换到正确的位置,同时把左边的都放上小的,右边都放上大的。这个算法每一次选取一个中心点,排序之后,查看中心点的位置。如果它的位置大于K,就说明,要在前面一个子序列中找出第K大的元素。反之,如果小于K,就说明要在后面一个序列中找出第k大的元素。
  • 复杂度分析: 
      这种算法的复杂度分析稍微复杂。第一次交换,算法复杂度为O(n),接下来的过程和快速排序不同,快速排序是要继续处理两边的数据,再合并,合并操作的算法复杂度是O(1),于是总的算法复杂度是O(nlogn)(可以这么理解,每次交换用了n,一共logn次)。但是这里在确定中心点的相对位置(在K的左边或者右边)之后不用再对剩下的一半进行处理。也就是说第二次插入的算法复杂度不再是O(n)而是O(n/2),然后接下来的过程是1+1/2+1/4+........<2,换句话说就是一共是O(2n)的算法复杂度也就是O(n)的算法复杂度。 
      这个算法是一种很经典的算法。原因是因为它通过努力把算法复杂度在每次递归中下降一些,最终让整个算法的复杂度下降极多,算是一种十分聪明的做法。
  • C++实现:
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
int __partition(int arr[],int l,int r){
swap(arr[l],arr[rand()%(r - l + 1) + l]);
int v = arr[l];
int j = l;
for(int i = l + 1;i <= r;i++){
if(arr[i] > v){
swap(arr[i],arr[j + 1]);
j ++;
}
}
swap(arr[l],arr[j]);
return j;
}
int __selection(int arr[],int l,int r,int k){
if(l == r)
return arr[l];
int p = __partition(arr,l,r);
if(p == k)
return arr[p];
else if(p > k)
return __selection(arr,l,p - 1,k);
else
return __selection(arr,p + 1,r,k);
}
int selection(int arr[],int n,int k){
srand(time(NULL));
return __selection(arr,0,n - 1,k - 1);//索引是从0开始的
}
int main(){
int arr[10] = {10,9,8,7,6,5,4,3,2,1};
int n = 10;
cout<<"数组中第3大的元素为:"<<selection(arr,n,3)<<endl;
}

 

  • Python实现:
import random
def __partition(arr,l,r):
k = random.randint(l,r)
arr[l],arr[k] = arr[k],arr[l]
v= arr[l]
j = l
for i in range(l + 1,r + 1):
if arr[i] < v:
arr[i],arr[j + 1] = arr[j + 1],arr[i]
j += 1
arr[l],arr[j] = arr[j],arr[l]
return j
def __selection(arr,l,r,k):
if l == r:
return arr[l]
p = __partition(arr,l,r)
if p == k:
return arr[p]
elif p > k:
return __selection(arr,l,p - 1,k)
else:
return __selection(arr,p + 1,r,k)
def selection(arr,k):
n = len(arr)
return __selection(arr,0,n - 1,k - 1)
arr = [10,9,8,7,6,5,4,3,2,1]
print('数组中第2小的元素为'+ str(selection(arr,2)))

推荐一个良心公众号【IT资源社】:

本公众号致力于免费分享全网最优秀的视频资源,学习资料,面试经验等,前端,PHP,JAVA,算法,Python,大数据等等,你想要的这都有

IT资源社-QQ交流群:625494093

也可添加微信拉你进微信群: super1319164238

微信搜索公众号:ITziyuanshe 或者扫描下方二维码直接关注,

 

 
posted @ 2018-04-10 14:22  Lovebugs.cn  阅读(509)  评论(0编辑  收藏  举报