归并排序和快速排序的衍生问题
前面两篇总结了常见的几种排序算法的主要思想以及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 或者扫描下方二维码直接关注,