什么是 two pointers  

  以一个例子引入:给定一个递增的正整数序列和一个正整数 M,求序列中的两个不同位置的数 a 和 b,使得它们的和恰好为 M,输出所有满足条件的方案。

  本题的一个最直观的想法是,使用二重循环枚举序列中的整数 a 和 b,判断它们的和是否为 M。时间复杂度为 O(n2)。

  two pointers 将利用有序序列的枚举特性来有效降低复杂度。它针对本题的算法如下:

  令下标 i 的初值为0,下标 j 的初值为 n-1,即令 i、j 分别指向序列的第一个元素和最后一个元素,接下来根据 a[i]+a[j] 与 M 的大小来进行下面三种选择,使 i 不断向右移动、使 j 不断向左移动,直到 i≥j 成立

    • 若 a[i]+a[j]==M ,说明找到了其中一种方案,令 i=i+1、j=j-1。
    • 若 a[i]+a[j]>M,令 j=j-1。
    • 若 a[i]+a[j]<M,令 i=i+1。

  反复执行上面三个判断,直到 i≥j 成立,时间复杂度为O(n),代码如下:

  1. while(i < j) {
  2. if(a[i]+a[j] == M) {
  3. printf("%d %d\n", i, j);
  4. i++; j--;
  5. } else if(a[i]+a[j] < M) {
  6. i++;
  7. } else {
  8. j--;
  9. }
  10. }

   

  再来看序列合并问题。假设有两个递增序列 A 与 B,要求将它们合并为一个递增序列 C。

  同样的,可以设置两个下标 i 和  j ,初值均为0,表示分别指向序列 A 的第一个元素和序列 B 的第一个元素,然后根据 A[i] 与 B[j] 的大小来决定哪一个放入序列 C。

    • 若 A[i]≤B[j],把 A[i] 加入序列 C 中,并让 i 加1
    • 若 A[i]>B[j],把 B[j] 加入序列 C 中,并让 j 加1    

  上面的分支操作直到 i、j 中的一个到达序列末端为止,然后将另一个序列的所有元素依次加入序列 C 中,代码如下:

  1. int merge(int A[], int B[], int C[], int n, int m) {
  2. int i=, j=, index=; // i指向A,j指向B,index指向C
  3. while(i<n && j<m) {
  4. if(A[i] <= B[j]) { // 若 A[i]≤B[j]
  5. C[index++] = A[i++];
  6. } else { // 若 A[i]>B[j]
  7. C[index++] = B[j++];
  8. }
  9. }
  10. while(i<n) C[index++] = A[i++]; // 若 A 有剩余
  11. while(j<m) C[index++] = B[j++]; // 若 B 有剩余
  12. return index; // 返回 C 长度
  13. }

  广义上的 two pointers 是利用问题本身与序列的特性,使用两个下标 i、j 对序列进行扫描(可以同向扫描,也可以反向扫描),以较低的复杂度(一般为 O(n) )解决问题。

归并排序

  归并排序是一种基于“归并”思想的排序方法,本节主要介绍其中最基本的 2-路归并排序。2-路归并排序的原理是,将序列两两分组,将序列归并为 n/2 个组,组内单独排序;然后将这些组再两两归并,生成 n/4 个组,组内再单独排序;以此类推,直到只剩下一个组为止。时间复杂度为 O(nlogn)。

  1. 递归实现

  只需反复将当前区间 [left,right] 分为两半,对两个子区间 [left,mid] 与 [mid+1, right] 分别递归进行归并排序,然后将两个已经有序的子区间合并为有序序列即可。代码如下:

  1. const int maxn = ;
  2. // 将数组A的 [L1,R1] 与 [L2,R2] 合并为有序区间(此处 L2=R1+1 )
  3. void merge(int A[], int L1, int R1, int L2, int R2) {
  4. int i=L1, j=L2;
  5. int temp[maxn], index=; // temp 临时储存合并序列
  6. while(i<=R1 && j<=R2) {
  7. if(A[i] <= A[j]) { // 若 A[i] ≤A[j]
  8. temp[index++] = A[i++];
  9. } else { // 若 A[i] > A[j]
  10. temp[index++] = A[j++];
  11. }
  12. while(i <= R1) temp[index++] = A[i++];
  13. while(j <= R2) temp[index++] = A[j++];
  14. for(int i=; i<index; ++i) {
  15. A[L1+i] = temp[i]; // 将合并后的序列赋值回 A
  16. }
  17. }
  18. }
  19. // 归并排序递归实现
  20. // 只需反复将当前区间 [left,right] 分为两半,对两个子区间 [left,mid] 与 [mid+1, right]
  21. // 分别递归进行归并排序,然后将两个已经有序的子区间合并为有序序列即可。
  22. void mergeSort(int A[], int left, int right) {
  23. if(left < right) { // 当 left==right 时,只有一个元素,认定为有序
  24. int mid = (left+right)/;
  25. mergeSort(A, left, mid); // 分为左区间和右区间
  26. mergeSort(A, mid+, right);
  27. merge(A, left, mid, mid+, right); // 将左区间和右区间合并
  28. }
  29. }

  2.非递归实现

  非递归实现主要考虑到这样一点:每次分组时组内元素个数上限都是2的幂次。于是就可以想到这样的思路:令步长 step 的初值为2,然后将数组中每 step 个元素作为一组,将其内部进行排序;再令 step 乘以2,重复上面的操作,直到 step/2 超过元素个数 n 。代码如下:

  1. const int maxn = ;
  2.  
  3. // 将数组A的 [L1,R1] 与 [L2,R2] 合并为有序区间(此处 L2=R1+1 )
  4. void merge(int A[], int L1, int R1, int L2, int R2) {
  5. int i=L1, j=L2;
  6. int temp[maxn], index=; // temp 临时储存合并序列
  7. while(i<=R1 && j<=R2) {
  8. if(A[i] <= A[j]) { // 若 A[i] ≤A[j]
  9. temp[index++] = A[i++];
  10. } else { // 若 A[i] > A[j]
  11. temp[index++] = A[j++];
  12. }
  13. while(i <= R1) temp[index++] = A[i++];
  14. while(j <= R2) temp[index++] = A[j++];
  15. for(int i=; i<index; ++i) {
  16. A[L1+i] = temp[i]; // 将合并后的序列赋值回 A
  17. }
  18. }
  19. }
  20.  
  21. // 归并排序非递归实现
  22. // 令步长 step 的初值为2,然后将数组中每 step 个元素作为一组,
  23. // 将其内部进行排序;再令 step 乘以2,重复上面的操作,直到 step/2 超过元素个数 n 。
  24. void mergeSort(int A[]) {
  25. // step 为组内元素个数
  26. for(int step=; step/ <= n; step *= ) {
  27. for(int i = ; i <= n; i += step) { // 对每一组,数组下标从1开始
  28. int mid = i + step/ -; // 左区间元素个数为 step/2
  29. if(mid+ <= n) { // 右区间存在元素
  30. // 左区间为 [left,mid],右区间为 [mid+1, min(i+step-1,n)
  31. merge(A, i, mid, mid+, min(i+step-, n));
  32. }
  33.  
  34. /*
  35. // 也可以用 sort 代替 merge 函数
  36. sort(A+i, A+min(i+step, n+1)); */
  37. }
  38. }
  39. }

快速排序

  快速排序的实现需要先解决这样一个问题:对一个序列 A[1]、A[2]、... 、A[n],调整序列中元素的位置,使得 A[1] (原序列中的 A[1])的左侧元素都不超过 A[1]、右侧所有元素都大于 A[1]。

  下面给出速度最快的做法,思想就是 two pointers:

     1. 先将 A[1] 存至某个临时变量 temp,并令两个下标 left、right 分别指向序列首尾。

     2. 只要 right 指向的元素 A[right] 大于 temp ,就将 right 不断左移;当某个时候 A[right] 小于等于 temp 时,将元素 A[right] 挪到 left 指向的元素 A[left] 处。

     3. 只要 left 指向的元素 A[right] 小于等于 temp ,就将 left 不断右移;当某个时候 A[right] 大于 temp 时,将元素 A[left] 挪到 right指向的元素 A[right] 处。

     4. 重复 2.3 ,直到 left 与 right 相遇,把 temp(也即原 A[1])放到相遇的地方。 

  1. // 对区间 [left,right]进行划分
  2. int Partition(int A[], int left, int right) {
  3. int temp = A[left]; // 1.
  4. while(left < right) {
  5. while(left<right && A[right]>temp) right--; // 2.
  6. A[left] = A[right];
  7. while(left<right && A[left]<=temp) left++; // 3.
  8. A[right] = A[left];
  9. }
  10. A[left] = temp; // 4.
  11. return left;
  12. }

       

  接下来就可以正式实现快速排序算法了。快速排序的思路是:

    1. 调整序列中的元素,使当前序列的最左端的元素在调整后满足左侧所有元素均不超过该元素、右侧所有元素均大于该元素

    2. 对该元素的左侧和右侧分别进行 1 的调整,直到当前调整区间的长度不超过 1

  快速排序的递归实现如下:

  1. // 快速排序
  2. void quickSort(int A[], int left, int right) {
  3. if(left < right) {
  4. int pos = Partition(A, left, right); // 1.
  5. quickSort(A, left, pos); // 2.
  6. quickSort(A, pos+, right);
  7. }
  8. }

  快速排序算法当序列中元素的排列比较随即时效率最高,但是当序列中元素接近有序时,会达到最坏时间复杂度 O(n2)。

  其中一种解决办法是随机选择主元,也就是对 A[left...right] 来说,不总是用 A[left] 作为主元,而是从 left...right 随机选择一个作为主元。

  下面来看看如何生成随机数。

    • 需要添加 stdlib.h 与 time.h 头文件。
    • 函数开头需加上  srand((unsigned)time(NULL)); ,这个语句将生成随机数的种子
    • rand() 只能生成 [0,RAND_MAX] 范围内的整数,RAND_MAX 在不同系统环境中不同,假设该系统为 32767
    • 如果想要输出给定范围 [a,b] 内的随机数,需要使用  rand() % (b-a+) + a
    • 如果需生成更大的数,例如 [a,b],b 大于 32767,可以使用  (int)(round(1.0*rand()/RAND_MAX*(b-a)+a))

  在此基础上继续讨论快排的写法。不妨生成一个范围在 [left,right] 内的随机数 p,然后以 A[p] 作为主元来进行划分。具体做法是:将 A[p] 与 A[left] 交换,然后按原先 Partition 函数的写法即可,代码如下:

  1. // 随机选择主元,然后进行划分
  2. int randPartition(int A[], int left, int right) {
  3. // 生成 [left,right] 内的随机数 p
  4. int p = (int)(round(1.0*rand()/RAND_MAX*(right-left) +left));
  5. swap(A[p], A[left]); // 交换 A[p] 和 A[left]
  6.  
  7. // 以下跟 Partition 完全相同
  8. return Partition(A, left, right);
  9. }

算法初步——two pointers的更多相关文章

  1. Scratch编程与高中数学算法初步

    scratch编程与高中数学算法初步 一提到编程,大家可能觉得晦涩难懂,没有一定的英语和数学思维基础的人,一大串的编程代码让人望而步,何况是中小学生.   Scratch是一款由麻省理工学院(MIT) ...

  2. 【原创】tarjan算法初步(强连通子图缩点)

    [原创]tarjan算法初步(强连通子图缩点) tarjan算法的思路不是一般的绕!!(不过既然是求强连通子图这样的回路也就可以稍微原谅了..) 但是研究tarjan之前总得知道强连通分量是什么吧.. ...

  3. 算法初步(julyedu网课整理)

    date: 2018-11-19 13:41:29 updated: 2018-11-19 14:31:04 算法初步(julyedu网课整理) 1 O(1) 基本运算 O(logn) 二分查找 分治 ...

  4. PageRank 算法初步了解

    前言 因为想做一下文本自动摘要,文本自动摘要是NLP的重要应用,搜了一下,有一种TextRank的算法,可以做文本自动摘要.其算法思想来源于Google的PageRank,所以先把PageRank给了 ...

  5. Dijkstra算法初步 - 迷宫问题

    你来到一个迷宫前.该迷宫由若干个房间组成,每个房间都有一个得分,第一次进入这个房间,你就可以得到这个分数.还有若干双向道路连结这些房间,你沿着这些道路从一个房间走到另外一个房间需要一些时间.游戏规定了 ...

  6. C语言之算法初步(汉诺塔--递归算法)

    个人觉得汉诺塔这个递归算法比电子老鼠的难了一些,不过一旦理解了也还是可以的,其实网上也有很多代码,可以直接参考.记得大一开始时就做过汉诺塔的习题,但是那时代码写得很长很长,也是不理解递归的结果.现在想 ...

  7. EM(期望最大化)算法初步认识

    不多说,直接上干货! 机器学习十大算法之一:EM算法(即期望最大化算法).能评得上十大之一,让人听起来觉得挺NB的.什么是NB啊,我们一般说某个人很NB,是因为他能解决一些别人解决不了的问题.神为什么 ...

  8. 算法初步---基本的数据结构(java为例)

    最近搞算法,觉得超级吃力的,一直以为数学好的,数学可以考试满分,算法一定没什么问题,贱贱地,我发现我自己想多了,还是自己的基础薄弱吧,今天我来补补最基础的知识. 算法(Algorithm)是指解题方案 ...

  9. Tarjan算法初步

    一.前置知识: 强连通分量:有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(stron ...

随机推荐

  1. canvas压缩图片成base64,传到后台解码需要注意的问题

    去除压缩完后的头部标志,data:imge一直到,位置,然后看看有没有空格,有的就替换成+号,传送的时候+号被http协议去掉了

  2. AS3中以post和get方式提交数据

    这里主要介绍在as3中用URLRequest对像来post或get数据到服务器. post用于大数据量的提交,get用于小数据量的提交. as3中提交数据: POST方式: 1.新建一个test.fl ...

  3. mysql的5.6版本支持分区吗?

    转载请注明出处:http://blog.csdn.net/dongdong9223/article/details/72291698 本文出自[我是干勾鱼的博客] 我们知道,查看mysql是否支持分区 ...

  4. 请求URL中有body怎么使用jmeter进行接口测试

    业务场景: 微信内免费领取激活码 1.点击“免费领取”按钮调取的接口 2.URL如下 https://yxyapi2.drcuiyutao.com/yxy-api-gateway/api/json/v ...

  5. VS 2010 转到COFF期间失败。

    可能的原因是framework 版本不匹配,我卸载4.5,装4.0后就解决了

  6. Golang使用MongoDB通用操作

    MongoDB是Nosql中常用的一种数据库,今天笔者就简单总结一下Golang如何使用这些通用的供能的,不喜勿喷... 研究的事例结构如下: type LikeBest struct { Autho ...

  7. hibernate的级联(hibernate注解的CascadeType属性)

    [自己项目遇到的问题]: 新增  删除都可以实现 ,就是修改的时候无法同步更新设计三个类:  问题类scask  正文内容类text类    查看数+回复数+讨论数的runinfo类 [正文类和查看数 ...

  8. bzoj 1500 维修序列

    Written with StackEdit. Description 请写一个程序,要求维护一个数列,支持以下 \(6\) 种操作: 请注意,格式栏 中的下划线' _ '表示实际输入文件中的空格 I ...

  9. linux离线搭建Python环境及安装numpy、pandas

    1.安装python2.7.3 Cent OS 6.5默认装的有python2.6.6,需要重新安装python2.7.3下载地址:https://www.python.org/downloads/s ...

  10. 《C#求职宝典》读书笔记

    王小科 电子工业出版 第一篇 面试求职第一步 一个例子:一支行军中的队伍长100米,一个传令兵从队尾跑至队头,再立即返回队尾,队伍正好前进了100米.假设队伍 和传令兵行进的速度恒定,问传令兵跑了多少 ...