算法分析-分治 归并排序,递归插入排序,二分查找

反正分治的套路就是 相同子问题,递归做,我之前有介绍express源码,其中的中间件使用就是用next()函数一直递归,想看的看我的express源码分析:

分治3步骤:

  1. 分解
  2. 处理
  3. 归并

下面给出归并排序的js代码:

 1 var A = [5, 2, 4, 6, 1, 3];
 2 var len = A.length - 1;
 3 
 4 MERGE_SORT(A, 0, len);
 5 
 6 A.forEach(function (element, index, arr) {
 7     console.log(index, "-----------", element);
 8 });
 9 
10 function MERGE_SORT(A, start, end) {
11     if (start < end) {
12         var middle = Math.floor((start + end) / 2); //向下去整
13         MERGE_SORT(A, start, middle); //左边递归
14         MERGE_SORT(A, middle + 1, end); //右边递归
15         MERGE(A, start, middle, end);
16     }
17 }
18 
19 function MERGE(A, start, middle, end) {
20     var Arr1 = A.slice(start, middle + 1),
21         Arr2 = A.slice(middle + 1, end + 1),//slice(start,end) end不包括,所以加1
22         len1 = Arr1.length,
23         len2 = Arr2.length,
24         i = 0,
25         j = 0;
26     for (i, j; i < len1 && j < len2;) {
27         (Arr1[i] < Arr2[j]) ? (A[start++] = Arr1[i++]) : (A[start++] = Arr2[j++]);
28     }
29 
30     while (i == len1 && j < len2) {
31 
32         A[start++] = Arr2[j++];
33     }
34     while (j == len2 && i < len1) {
35         A[start++] = Arr1[i++];
36     }
37 
38 }

像这种分治递归的,你需要找到一个出口来结束递归。

 

还记得插入排序吗,我们换成递归的写法,怎么写呢,思路是这样的,将A[n],插入A[0...n-1]内,而A[0..n-1]是已经排序好的。上代码:

 1 var A = [5, 2, 4, 6, 1, 3];
 2 
 3 RECURSIVE_INSERT_SORT(A, 6);
 4 A.forEach(function (element, index, arr) {
 5     console.log(index, "-----------", element);
 6 });
 7 
 8 function RECURSIVE_INSERT_SORT(A, len) {
 9     if (len > 1) {
10         var last_var = A[len - 1];
11         A.splice(--len, 1);
12         RECURSIVE_INSERT_SORT(A, len);
13         INSERT(A, last_var);
14     }
15 }
16 
17 function INSERT(A, last_var) {
18     var len = A.length,
19         k = len - 1;
20     while (A[k] > last_var && k >= 0) {
21 
22         A[k + 1] = A[k];
23         k--;
24     }
25     A[k + 1] = last_var;
26 
27 }

 

那思考一下,如果A=[1,2,3,5,6,7]是有序的,用二分查找去查找v=3,v=4,怎么写,有思路吗?

 1 var result = {index: false},
 2     A = [1, 2, 3, 5, 6, 7];
 3 function BINARY_SEARCH(A, start, end, val) {
 4     if (end >= start) {
 5         var middle = Math.floor((start + end) / 2);
 6         if (A[middle] == val) {
 7             result.index = middle;
 8             console.log("找到了~");
 9             return;
10         }
11         if (A[middle] < val) {
12 
13             BINARY_SEARCH(A, middle + 1, end, val);
14         }
15         if (A[middle] > val) {
16             BINARY_SEARCH(A, start, middle - 1, val);
17         }
18     }
19 }
20 
21 BINARY_SEARCH(A, 0, 5, 7);
22 console.log(result.index);

 

既然我们已经学会了二分查找策略,我们能不能将插入排序再改一改,将while循环改成二分查找策略呢?

 

 1 var A = [99, 12, 77, 103, 1000, 3, 11, 4324, 3, 321, 545, 65, 76765, 78, 889, 98, 324, 23, 4, 544, 6, 2];
 2 
 3 BINARY_INSERT_SROT(A);
 4 A.forEach(function (element, index, arr) {
 5     console.log(index, "-----------", element);
 6 });
 7 
 8 function BINARY_INSERT_SROT(A) {
 9     var len = A.length;
10     for (var i = 1; i < len; i++) {
11         var key = A[i];
12         var j = i - 1;
13 
14         /*
15          *  
16          *  A是要修改的数组对象
17          *  0表示已经排号序列的start下标,
18          *  j表示已经排号序列的end下标,
19          *  key 表示需要插入的值,也就是第五个参数need_insert_index下标对应的值
20          *   need_insert_index 就是我们要插入的key值的下标。也就是j+1;
21          * 
22          * */
23         BINARY_SEARCH(A, 0, j, key, j + 1); //这里替换掉while(key<A[j]&&j>=0)
24     }
25 }
26 function BINARY_SEARCH(A, start, end, val, need_insert_index) {
27 
28     /*
29     * 
30     * 我们知道二分查找是查找中间下标对于的值是否为所求,是就找到,不是,就对半再递归查找。
31     * 
32     * 这一步在我们这个程序里还是需要的,毕竟可能会直接找到相同的,找到后,我们默认将值插在后面,
33     * 
34     * 这里有个关键步骤,也就是第5个参数的用处,我们是通过对比查找的。我们如果往后移动,也一定要在
35     * 
36     * 这个middle下标到need_insert_index下标之间全部移动,不然可能只移动了一部分。
37     * 
38     * 当start==end的时候,或者start + 1 ==end的时候,middle都等于stsrt,这时候,其实都是比较一个数,
39     * 
40     * 下标的移动和middle这个找到的特殊情况一样,当找不到的时候根据条件去移动。
41     * 
42     * */
43     
44     
45     if (end >= start) {
46         var middle = Math.floor((start + end) / 2);
47         if (A[middle] == val) {
48             var j = middle;
49             while (need_insert_index > middle) {
50                 A[need_insert_index] = A[need_insert_index - 1];
51                 need_insert_index--;
52             }
53             A[middle + 1] = val; //默认是插后的
54             return;
55         }
56         if (middle == start) {
57             if (A[start] <= val && A[end] >= val) {
58                 while (need_insert_index > start) {
59                     A[need_insert_index] = A[need_insert_index - 1];
60                     need_insert_index--;
61                 }
62                 A[start + 1] = val;
63 
64             } else if (A[start] <= val && A[end] <= val) {
65                 while (need_insert_index > end) {
66                     A[need_insert_index] = A[need_insert_index - 1];
67                     need_insert_index--;
68                 }
69                 A[end + 1] = val;
70             } else {
71                 while (need_insert_index > start) {
72                     A[need_insert_index] = A[need_insert_index - 1];
73                     need_insert_index--;
74                 }
75                 A[start] = val;
76             }
77             return
78         }
79         (A[middle] > val) ? (BINARY_SEARCH(A, start, middle - 1, val, need_insert_index)) : (BINARY_SEARCH(A, middle + 1, end, val, need_insert_index));
80     }
81 }

 思考:请给出一个运行时间为O(nlgn)的算法,使之能在给定一个由n个整数构成的集合S和另一个整数x时,判断出S中是否存在有两个其和等于x的元素。

分析:

       若要整个算法的时间复杂度为O(nlgn),那么只要算法中最复杂的模块的复杂度为O(nlgn)就可以了。

 1 var A = [99, 12, 77, 103, 1000, 3, 11, 4324, 3, 321, 545, 65, 76765, 78, 889, 98, 324, 23, 4, 544, 6, 2],
 2     result = {index: false};
 3 var isfind = isExisted(A, 89);
 4 console.log(isfind);
 5 
 6 function isExisted(A, val) {
 7     var len = A.length;
 8     //先给集合排序
 9     MERGE_SORT(A, 0, len - 1);
10 
11     //循环次数最多为n
12     for (var i = 0; i < len; i++) {
13         //每次次二分搜索,时间消耗lgn
14         BINARY_SEARCH(A, 0, len - 1, val - A[i]);
15         if (result.index) {
16             //下面这个判断是考虑到了x的值是序列中某个元素的2倍的情况
17             if (result.index != i) {
18                 result.index = true;
19                 break;
20             }else {
21                 result.index = false;
22             }
23         }
24     }
25     return  result.index;
26 }
27 
28 
29  //复制粘贴
30 function BINARY_SEARCH(A, start, end, val) {
31     if (end >= start) {
32         var middle = Math.floor((start + end) / 2);
33         if (A[middle] == val) {
34             result.index = middle;
35             console.log("找到了~");
36             return;
37         }
38         if (A[middle] < val) {
39 
40             BINARY_SEARCH(A, middle + 1, end, val);
41         }
42         if (A[middle] > val) {
43             BINARY_SEARCH(A, start, middle - 1, val);
44         }
45     }
46 }
47 
48  //复制粘贴
49 function MERGE_SORT(A, start, end) {
50     if (start < end) {
51         let middle = Math.floor((end + start) / 2);
52         MERGE_SORT(A, start, middle);
53         MERGE_SORT(A, middle + 1, end);
54         MERGE(A, start, middle, end);
55     }
56 }
57 
58  //复制粘贴
59 function MERGE(A, start, middle, end) {
60     var arr1 = A.slice(start, middle + 1);
61     var arr2 = A.slice(middle + 1, end + 1);
62     var len1 = arr1.length;
63     var len2 = arr2.length;
64     var i = 0;
65     var j = 0;
66     for (i, j; i < len1 && j < len2;) {
67         (arr1[i] < arr2[j]) ? (A[start++] = arr1[i++]) : (A[start++] = arr2[j++]);
68     }
69     while (i == len1 && j < len2) {
70         A[start++] = arr2[j++];
71     }
72     while (j == len2 && i < len1) {
73         A[start++] = arr1[i++];
74     }
75 }

总结:

          merge_sort这个函数是归并排序算法,它的时间复杂度是O(nlgn)。

           第12行处,这个for循环中,有一个二分查找算法。它的时间复杂度是O(lgn),所以整个for循环模块的时间复杂度为O(nlgn)。

 

霍纳规则的正确性:

 

公式的简单推理:

NewImagea0+a1*x+a2*x^2+a3*x^3+a4*x^4+…+ak*x^k+…+an*x^n

NewImage

 

 

计算机的循环计算。

1      y = 0                                      时间花费  1

2      for i=n down to 0                                   n+1

3              y = ai + x*y                                     n

                                                                        总时间花费  2n+2

这样循环计算出来的y就是上面汇总的值。

 

a)、Θ(n), 推理过程看上面。

b)、伪代码实现的朴素的多项式求值算法。

 

下面是一个取巧的算法,时间消耗是 3n, 在n >2 时 时间消耗大于 2n+2

void Ploynomial()                                  时间消耗 = 3n
{
        int t;                                                      1
        sum = a[0];                                          1
        for (i = 1; i < n; i++)                            n
        {
                sum += a[i]*x;                             n-1
                x = x*x;                                         n-1
        }
}

c)、

初始化: 有 y = 0, i = n , 这样 计算 下面公式的右边 为 0 , 所以初试化满足循环不变式。 

NewImage

          

保持:假设当第i=j满足时,考察i=j-1。

终止: 当循环结束时候,有 i= -1,

 

------------

由于0从0到n-(i+1),因此有:
y = Σ ak+i+1 * x^k
  = ak+i+1 + ak+i+2 * x + ... + an * x^(n-(i+1))
霍纳规则代码段循环不变式证明如下:
初始:
    i=n,y[n] = 0,迭代开始时,循环后有y[n] = a[n]。
保持:
    对于任意 0 ≤ i ≤ n,循环后有:
        y[i] = a[i] + y[i+1] * x = a[i] + (a[i+1] * x + a[i+2] * x + ... + a[n] * x^(n-(i+1))) * x
             = a[i] + a[i+1] * x + a[i+2] * x^2 + ... + a[n] * x^(n-i)
终止:
    i小于0时终止,此时有 y[0] = a[0] + a[1] * x + a[2] * x^2 + a[n] * x^n

证明和y = Σ a[k+i+1] * x^k的关系:
    k 从0到n-(i+1),等价于 0 ≤ k ≤ n-(i+1)。因此
        y = Σ a[k+i+1] * x^k
            = a[i+1] + a[i+2] * x + ... + a[n-(i+1)+i+1] * x^(n-i)
            = a[i+1] + a[i+2] * x + ... + a[n] * x^(n-i)
    由于i+1循环之后和i循环之前的值相等,用y'[i]表示i循环之前的值,则有:
        y'[i] = y[i+1]
    霍纳规则循环不变式的结果表明:
        y[i] = a[i] + a[i+1] * x + a[i+2] * x^2 + ... + a[n] * x^(n-i)
    因此有:
        y'[i] = y[i+1] = a[i+1] + a[i+2] * x + ... + a[n] * x^(n-(i+1))
    令k=n-(i+1),则n=k+i+1,所以:
        y'[i] = a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^(k+i+1-(i+1))
                = a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^k
    用y表示y'[i],则有:
        y = a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^k
            = Σ a[k+i+1] * x^k
    其中 k从0到n-(i+1)
    证毕。

 

思考题:逆序对
设A[1..n]是一个包含n个不同数的数组。如果i<j且A[i]>A[j],则(i,j)就称为A中的一个逆序对(inversion)。
a)列出数组〈2,3,8,6,1〉的5个逆序。
b)如果数组的元素取自集合{1, 2, ..., n},那么,怎样的数组含有最多的逆序对?它包含多少个逆序对?
c)插入排序的运行时间与输入数组中逆序对的数量之间有怎样的关系?说明你的理由。
d)给出一个算法,它能用Θ(nlgn)的最坏情况运行时间,确定n个元素的任何排列中逆序对的数目。(提示:修改合并排序)

a)  (2,1)  (3,1) (8,1) (6,1),(8,6)

b) 数组从大到小有序排列时,逆序对最多,为n(n-1)/2个。

c) 逆序对增加时,插入排序时间增加。
没有逆序对时,插入排序时间最少,为Θ(n)。
逆序对最多时,插入排序时间最多,为Θ(n^2)。

d)  归并算法, 每次移动牌,次数加1, 合计的次数就是逆序对的个数。

给出修改后的程序:

 1 var num = 0,A = [5,2,4,6,1,3]; 2 MERGE_SORT(A, 0, A.length - 1);
 3 console.log("最终结果是:",num);
 4 
 5 function MERGE_SORT(A, start, end) {
 6     if (start < end) {
 7         let middle = Math.floor((end + start) / 2);
 8         MERGE_SORT(A, start, middle);
 9         MERGE_SORT(A, middle + 1, end);
10         MERGE(A, start, middle, end);
11     }
12 }
13 
14 function MERGE(A, start, middle, end) {
15     var arr1 = A.slice(start, middle + 1);
16     var arr2 = A.slice(middle + 1, end + 1);
17     var len1 = arr1.length;
18     var len2 = arr2.length;
19     var i = 0;
20     var j = 0;
21     for (i, j; i < len1 && j < len2;) {
22         /*  (arr1[i] < arr2[j]) ? (A[start++] = arr1[i++]) : (A[start++] = arr2[j++]);*/
23         if (arr1[i] > arr2[j]) {
24             num += (len1 - i);
25             A[start++] = arr2[j++];
26 
27         } else {
28             A[start++] = arr1[i++]
29         }
30     }
31     while (i == len1 && j < len2) {
32         A[start++] = arr2[j++];
33 
34     }
35     while (j == len2 && i < len1) {
36         A[start++] = arr1[i++];
37 
38     }
39     console.log(A, "对应的num数目:",num);
40 
41 }

分析:

    我们使用归并排序,其实递归执行的时候 ,是从左往右的,大家可以画图:

                           [5,2,4,6,1,3]

                       [5,2,4]        [6,1,3]

                  [5,2]   [4]      [6,1]  [3]

              [5] [2]             [6]  [1] 

     我们发现,当归并[5] [2] 的时候,因为左边大于右边,所以数目加1,归并后变成[2,5],[2,5]和[4]归并,因为5大于4,数目加1变为2,

   归并后为[2,4,5],同理分析右边,最后归并[2,4,5] [1,3,6] 这里是关键,也就是为什么代码中是绿色的部分,因为2大于1,所以2后面的所有都大于1

 4大于3,4后面的全大于3.

 

posted @ 2016-09-23 17:10  hdu胡恩超  阅读(837)  评论(0编辑  收藏  举报