Moso's profile魔战士PhotosBlogLists Tools Help

Blog


    October 26

    你离开的那一天

    你离开的那一天,我不能在你身边。 黄昏下没有色彩,茫然唯有茫然。
    你将要离开,唠唠不休的是我的童年。破碎而错位的记忆,依然勾画我的身影。
    你是那样的亲切,那样的善良。所有美好的回忆,从你这里开始。
    你走了,我不能去送你。但我知道,
    我知道,未来的日子里
    我会时常在梦里流泪
    体会极致的悲伤
    一生
     
     
     
     
    October 22

    选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。

    这几天笔试了好几次了,连续碰到一个关于常见排序算法稳定性判别的问题,往往还是多选,对于我以及和我一样拿不准的同学可不是一个能轻易下结论的题目,当然如果你笔试之前已经记住了数据结构书上哪些是稳定的,哪些不是稳定的,做起来应该可以轻松搞定。本文是针对老是记不住这个或者想真正明白到底为什么是稳定或者不稳定的人准备的。

          首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。

         其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。

         回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。

       (1)冒泡排序

            冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。

    (2)选择排序

          选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

    (3)插入排序
         插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

    (4)快速排序
        快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻。

    (5)归并排序
        归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

    (6)基数排序
       基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

    (7)希尔排序(shell)
        希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

    (8)堆排序
       我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

     

    October 19

    最长上升子序列

    这是一个很好的题目。题目的算法还是比较容易看出来的,就是求最长上升子序列的长度。不过这一题的数据规模最大可以达到40000,经典的O(n^2)的动态规划算法明显会超时。我们需要寻找更好的方法来解决是最长上升子序列问题。
      先回顾经典的O(n^2)的动态规划算法,设A[i]表示序列中的第i个数,F[i]表示从1到i这一段中以i结尾的最长上升子序列的长度,初始时设F[i] = 0(i = 1, 2, ..., len(A))。则有动态规划方程:F[i] = max{1, F[j] + 1} (j = 1, 2, ..., i - 1, 且A[j] < A[i])。
      现在,我们仔细考虑计算F[i]时的情况。假设有两个元素A[x]和A[y],满足
       (1)x < y < i (2)A[x] < A[y] < A[i] (3)F[x] = F[y]
      此时,选择F[x]和选择F[y]都可以得到同样的F[i]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是应该选择A[y]呢?
      很明显,选择A[x]比选择A[y]要好。因为由于条件(2),在A[x+1] ... A[i-1]这一段中,如果存在A[z],A[x] < A[z] < a[y],则与选择A[y]相比,将会得到更长的上升子序列。
      再根据条件(3),我们会得到一个启示:根据F[]的值进行分类。对于F[]的每一个取值k,我们只需要保留满足F[i] = k的所有A[i]中的最小值。设D[k]记录这个值,即D[k] = min{A[i]} (F[i] = k)。
      注意到D[]的两个特点:
      (1) D[k]的值是在整个计算过程中是单调不上升的。
      (2) D[]的值是有序的,即D[1] < D[2] < D[3] < ... < D[n]。
      利用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长上升子序列长度为len。先判断A[i]与D[len]。若A[i] > D[len],则将A[i]接在D[len]后将得到一个更长的上升子序列,len = len + 1, D[len] = A[i];否则,在D[1]..D[len]中,找到最大的j,满足D[j] < A[i]。令k = j + 1,则有D[j] < A[i] <= D[k],将A[i]接在D[j]后将得到一个更长的上升子序列,同时更新D[k] = A[i]。最后,len即为所要求的最长上升子序列的长度。
      在上述算法中,若使用朴素的顺序查找在D[1]..D[len]查找,由于共有O(n)个元素需要计算,每次计算时的复杂度是O(n),则整个算法的时间复杂度为O(n^2),与原来的算法相比没有任何进步。但是由于D[]的特点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法的时间复杂度下降为O(nlogn),有了非常显著的提高。需要注意的是,D[]在算法结束后记录的并不是一个符合题意的最长上升子序列!
    October 04

    quick_sort非递归版

    先贴个最笨的,stack空间会涨得很快,最差达到O(n).
    A,0,r.A是数组初址,0是左限,r是右限);
    q = partition( A,0,r); //q初次分成两半
    void quick_sort_iter1()
    {
     stack s;
     s.push(0,q-1);s.push(q+1,r);//push入两个范围
     while(!stack.isempty())
     {
      int left = s.pop,right=s.pop;
      int qq = partition( A,left,right);
      if(qq>left+1)
      s.push(left,qq-1);
      if(qq<right-1)
     s.push(qq+1,right);
     }
     
    改进的方法,在于,压栈时,先压大的区间,再压小的区间
    这样可以保证小区间先被pop处理,使栈的空间最大只有O(lgn).
    October 02

    生日paradax, 收集者问题,硬币问题,在线招聘者问题

    CLRS 5.4有几个经典的概率例子,包括了前面的牺牲品是必须品的问题。摘要如下:
    1. Birthday Paradax
    party上有30人,他们中有两人同一天生意的概率是多少?
    答案是70%+.你可能很吃惊,因为它不是你预料的30/365=8.1%.
    分析如下:30个人的生日都是独立并且均匀随机分布的。第1个天的生意为某天,第2天与之不同日的概率为
    364/365,第3天与前两天不同的概率为363/365.最终30人全不同的概率为II(365-i)/365(i为1到29)。
    1减去它就得0.7+.
     
    2.收集者问题。
    某公司在出售的食品中附送了生肖卡,每份一张,概率均等。为收集全12生肖,预期需要买多少包?
    答案是blnb,b是个数,在这里是12.
    分析:我们称买到1张未收集到的卡为1 hit.买第一包时hit率为1,第二包时为(b-1)/b,第i包时为(b-i+1)/b.
    于是所有的的hit为b/b+b/(b-1)+b/(b-2)+...b/(b-b+1)=blnb+O(1).
     
    3.连续投N次币,连续向上的次数,最大有可能是多少(预期)。
    答案lnN/ln2.
     
    4.问题同前文牺牲品是必须品。
    分析如下:
    N个人中,前K个被抛弃。最佳人选在第i个被选中的概率为(i>K): k/(i-1)*1/n.前者表示,前i-1个人中,最优者出现在前K个中。后者表示,第i个为最佳的可能性为1/n.
    所有,挑中最佳的概率为:IIk/(i-1)*1/n (i从K+1到n).
    得k/n(lnn-lnk).对K求导,发现当K=n/e时,有最大值,1/e.
    October 01

    CLRS习题5.1-3,比较不错

    Exercises 5.1-3:
    Start example

    Suppose that you want to output 0 with probability 1/2 and 1 with probability 1/2. At your disposal is a procedure BIASED-RANDOM, that outputs either 0 or 1. It outputs 1 with some probability p and 0 with probability 1 - p, where 0 < p < 1, but you do not know what p is. Give an algorithm that uses BIASED-RANDOM as a subroutine, and returns an unbiased answer, returning 0 with probability 1/2 and 1 with probability 1/2. What is the expected running time of your algorithm as a function of p?

    题意:假设你希望以各1/2的比率输出0和1。你可以自由使用一个输出0或者1的过程BIASED-RAND
    OM.它以概率p输出1,以概率1-p输出0,但是你并不知道p的值。给出一个利用BAISED-RAN
    DOM作为子程序的算法,返回一个无偏向结果,即以概率1/2返回0,以概率1/2返回1。作为
    p的函数,你的算法期望运行时间是多少?
    答案:
     连续两次调用BIASED-RANDOM函数,得到四种结果,策略分别如下:

    01:输出0
    00或者11:重新调用两次BIASED-RANDOM函数(即重新调用本函数)
    10:输出1

    则:
    重新调用本函数,即得到00或者11的概率pr = p^2+(1-p)^2
    输出0的概率p0 = sum((1-p)*p*pr^i, i=0..infinity) = 1/2
    输出1的概率p1 = sum(p*(1-p)*pr^i, i=0..infinity) = 1/2
    并可以算出调用BIASED-RANDOM的次数的数学期望:
    E(x) = sum((1-pr)*pr^(i-1)*2*i, i=1..infinity) = 2/(1-pr)
    此算法实际上是利用01,10出现概率的等价性,同时巧妙的将不等价的00,11转换到01,
    10,从而消除了其概率不等价性。

     

    牺牲品是必须品

    牺牲品是必须品。费话不多说,直接正题。
    有家公司招人,只录一个,有10人面试。这个面试很特殊,有如下规则:
    1.每面完一个人,就要当场决定是否录用。如录用,后面的人则不再面试。
    2.不能吃回头草。
    公司想得到最佳人选,他们采取怎样的策略呢?
    答案是,前4个面试的人无论好坏,全部否决。
    从第5个开始,如果有人比前4个任何一个都好,就录用。如果一直没有,则录用最后1人。
    事实上,如果有N个人面试,将采取如下策略:
    1.面前面的[n/e]个,全拒
    2.开始面后面的人,如果有人比前面的n/e个都好,录用。
    3.如果一直没有,则录用最后一个。
    理论证明如下:
    前k-1个全锯,从第k个开始比前面那些牺牲品都好的就是真命天子。
    不同的k取到最好的概率是
    {(k-1)/n}*{[1/(k-1)]+1/k+[1/(k+1)]+...+1/(n-1)}
    当n趋于无穷时,最佳的k趋于n/e。取得最好的概率是1/e.
    所以说,牺牲品是必须品。