美文网首页Swift
算法开篇课

算法开篇课

作者: Stark_Dylan | 来源:发表于2016-12-30 17:59 被阅读688次

    title: 算法开篇课
    date: 2016-10-14 13:13:53
    tags: 算法
    thumbnail: http://img2.imgtn.bdimg.com/it/u=4054526222,762120886&fm=21&gp=0.jpg


    算法开篇介绍:Algorithm,是指解题方案的准确而完整的描述,代表着用系统的方法描述解决问题的策略机制。

    最快最简单的排序

    首先看题:

    班上有5位同学,分别考了5分,3分,5分,8分,2分,将分数从大到小排序是8,5,5,3,2。有什么好的办法可以编写一段程序,让计算机随机读入5个分数然后将这5个分数从大到小输出。老道的程序员可能各种冒泡,打擂台,这都比较深入了,先往下看:

    我们借助一个一维数组就可以解决这个问题,创建一个a[11]的数组,这样下标分别为a[0]->a[10],分别表示0分,1分...10分,每当有一个分数出现,就在对应的下标位置+1,最后打印即可满足我们现在的要求。

    int main(int argc, const char * argv[]) {
      
      int a[11];
      int scannedNumber;
      
      for (int i = 0; i < 11; i ++) {
        a[i] = 0;
      }
      
      for (int j = 0; j < 5; j ++) {
        scanf("%d", &scannedNumber);
        a[scannedNumber] ++;
      }
      
      for (int k = 10; k >= 0; k --) {
        for (int l = 0; l < a[k]; l ++) {
          printf("%d", k);
        }
      }
      
      return 0;
    }
    

    这种排序算法,我们称为桶排序,每个分数都好比一个桶,每出现一次,就在桶中加一点东西。接下来,我们尝试着对数据范围在0~100之间的任意数量数字进行从大到小的排序:

    int main(int argc, const char * argv[]) {
      int book[101], scannedNumber;
      int inputCount;
      
      for (int i = 0; i < 101; i ++) {
        book[i] = 0;
      }
      
      printf("Enter numbers you want:");
      scanf("%d", &inputCount);
      
      for (int j = 0; j < inputCount; j ++) {
        scanf("%d", &scannedNumber);
        book[scannedNumber] ++;
      }
      
      for (int k = 100; k >= 0; k --) {
        for (int l = 0; l < book[k]; l ++) {
          printf("%d ", k);
        }
      }
      
      return 0;
    }
    

    同样没有什么问题的,我们接下来考虑时间复杂度的问题:代码中第6行,循环了M次(M为桶的个数),第14行循环了N次(输入的数字个数),19行循环了M+N次,所以我们得到时间复杂度为O(2*(M+N)),在说时间复杂度的时候,可以忽略较小的常数,所以最终的时间复杂度为:

    O(M+N)
    

    这是一个非常快的排序算法,其实这还不是真正的桶排序,桶排序实际要更复杂,这个只能算简化版。基本上还不能算是一个真正意义的算法,上边的处理如果碰到输出得该分数的同学的信息就显得有点问题了,因为我们只是输出了分数。所以引出第二节:冒泡排序

    冒泡排序

    简化版的桶排序,不仅仅有使用范围的限制,更是浪费空间,如果我们需要排序的范围是0~2100000000,那就要申请2100000001个变量,我们要用这么多的桶来存储每一个数字出现的次数。即时,你只给3个数字(1, 19999, 1999999)排序,也需要2000001个桶,太浪费了。还不止,如果是浮点型呢?那我们接触一个新的算法,冒泡排序!它可以很好的解决这2个问题。冒泡排序的基本思想是,相邻的2个元素,如果顺序错误就把他们位置互换。如果我们要将

    12, 35, 99, 18, 76
    

    这几个数从大到小进行排序,越小的越靠后。

    首先:比较第1位和第2位的大小,我们发现12要比35小,所以交换位置,交换后:

    35, 12, 99, 18, 76
    

    然后:按照上述的方法,继续往下比较,第2位与第3位....

    35, 99, 12, 18, 76
    

    继续:第3位和第4位:

    35, 99, 18, 12, 76
    

    继续:第4位和第5位:

    35, 99, 18, 76, 12
    

    经过4次比较之后,最小的数字已经到了最后一位,一位一位的比,大的就往前换,就像一个气泡,所以称为冒泡。到这里,我们只是将其中的一个数字归位了,继续重复上面的过程:继续比较第1位与第2位,第2位与第3位,第3位与第4位,你会发现,第5位不需要比较了,因为他已经是最小的了。第二次比较后:

    99, 35, 76, 18, 12
    

    接下来的几次都是这样,有几个数字,比较的次数是n-1趟。冒泡的原则是,每次比较只将1个数组归位。接下来我们代码实现:

    int main(int argc, const char * argv[]) {
      int a[20], scannedNumber, input;
      
      scanf("%d", &input);
      
      for (int i = 1; i <= input; i ++) {
        scanf("%d", &scannedNumber);
        a[i] = scannedNumber;
      }
      
      // 共有INPUT个数字,所以只需要排INPUT-1次即可
      for (int i = 1; i <= input - 1; i ++) {
        
        // INPUT-I 意味着,每一次外层循环后,都有一个数字归位
        for (int j = 1; j <= input - i ; j ++) {
          
          // 交换
          if ( a[j] < a[j + 1] ) {
            int temp = a[j];
            a[j] = a[j + 1];
            a[j + 1] = temp;
          }
        }
      }
      
      for (int i = 1; i <= input; i ++) {
        printf("%d ", a[i]);
      }
      
      return 0;
    }
    

    将上边的代码稍稍修改,就可以解决前面桶排序无法做到的输出学生信息以及浮点数的问题:

    struct Student {
      char name[20];
      float score;
    };
    
    int main(int argc, const char * argv[]) {
      
      struct Student s[100];
      int scannedNumber;
      
      scanf("%d", &scannedNumber);
      
      for (int i = 1; i <= scannedNumber; i ++) {
        scanf("%s %f", s[i].name, &s[i].score);
      }
      
      // 控制排序次数
      for (int j = 1; j <= scannedNumber - 1; j ++) {
        for (int k = 1; k <= scannedNumber - j; k ++) {
          if (s[k].score < s[k+1].score) {
            struct Student temp = s[k];
            s[k] = s[k + 1];
            s[k + 1] = temp;
          }
        }
      }
      
      for (int l = 1; l <= scannedNumber; l ++) {
        printf("%s, %.2f\n", s[l].name, s[l].score);
      }
      
      return 0;
    }
    

    完美搞定了。输入:

    dylan 81.5
    alice 64.6
    peter 91.4
    bobo 31.9
    amy 67
    

    输出:

    peter, 91.40
    dylan, 81.50
    amy, 67.00
    alice, 64.60
    bobo, 31.90
    

    冒泡排序的核心部分是嵌套循环,不难看出,冒泡循环的复杂度是:

    O(N^2)
    

    这是一个很高的时间复杂度,冒泡排序除了它迷人的名字,以时间复杂度来看,没什么好推荐的,想要更好的排序么?

    快速排序

    冒泡排序算是我们学习的第一个算法,但是时间浪费的极多。假定计算机运行10亿次/秒,桶排序需要10亿+1个位置,但只需要0.1s,冒泡排序则需要1千万秒。接下来我们了解快速排序。首先记住快速排序的核心方法:设,基准数为左边第一个数,保证基准数左边全部比它小,右边全部比它大;所以2个起点,最左边、最右边。共同向中间触发,右边先走(注意,左边为基准数,一定要先从右往左,先想想为什么,后面给出解释),先从右边向左边找一个比基准数小的数字,然后从左边向右边找一个比基准数大的数字,交换他们。当左右碰面的时候,交换当前数字与基准数的位置。当基准数确认位置之后,基准数左边、右边分为2个子列(有一点点2分味道,以后讲),在进行设置基准数,寻找。这里用到了递归,所以我们来模拟一下过程,假定有这样一个数列:

    6, 1, 2, 7, 9, 3, 4, 5, 10, 8
    

    我们以最左边的6为初始基准数,从左向右找一个大于6的数,从右向左找一个小于6的数字,并交换,我们发现,从右向左第一个小于6的数字是5,从左向右第一个大于6的数字是7,交换他们的位置

    6, 1, 2, 5, 9, 3, 4, 7, 10, 8
    

    接下来,继续向前寻找,左向右发现了9,右向左发现了4,所以交换他们:

    6, 1, 2, 5, 4, 3, 9, 7, 10, 8
    

    继续往前,糟了,2个人撞一起了,这个时候,把基准数与这个数字交换位置:

    3, 1, 2, 5, 4, 6, 9, 7, 10, 8
    

    这样,基准数字6已经归位,而且左边都是小于6的数字,右边都是大于6的数字。接下来,我们分别处理左边、右边两个数列:

    3, 1, 2, 5, 4
    9, 7, 10, 8
    

    先处理第一个序列,3为基准数:

       i→      ←j
    3, 1, 2, 5, 4
          得
    2, 1, 3, 5, 4
          得
    子串1: 2, 1   -> 1, 2
    
    子串2: 5, 4   -> 4, 5
    

    因为是右边先开始,所以在2的位置,会碰撞,交换位置即得到了结果。子串9, 7, 10, 8大家自己去分析一下。最终排序结束了。然后我们按照上边的思想来写代码:

    int a[11];
    int count = 10;
    void sort(int left, int right);
    
    int main(int argc, const char * argv[]) {
      
      for (int i = 1; i <= count; i ++) {
        scanf("%d", &a[i]);
      }
      
      sort(1, count);
      
      for (int j = 1; j <= count; j ++) {
        printf("%d ", a[j]);
      }
      
      return 0;
    }
    
    void sort(int left, int right) {
      if ( left > right ) {
        return;
      }
      
      // 设置基准数
      int refNum = a[left];
      
      // 设置左右起点
      int lPoint = left;
      int rPoint = right;
      
      // 当没有碰撞的时候,不停的交换
      while (lPoint != rPoint) {
        // 从右边开始找,比基准数小的数字, 然后停止,注意我的判断,如果比基准数大,rPoint向前移动一位,也就是说,如果比基准数字小,rPoint记录的就是这个数字的位置
        while (a[rPoint] >= refNum && lPoint < rPoint) {
          rPoint --;
        }
        
        // 左边找,比基准数大的数字,然后停止
        while (a[lPoint] <= refNum && lPoint < rPoint ) {
          lPoint ++;
        }
        
        // 交换位置
        if ( lPoint < rPoint ) {
          int temp = a[lPoint];
          a[lPoint] = a[rPoint];
          a[rPoint] = temp;
        }
      }
      
      // 归位基准数字,这个时候,左右标记点处于同一个位置上,也就是基准点应该在的位置
      a[left] = a[lPoint];
      a[lPoint] = refNum;
      // 交换之后,开始处理子串,基准数不需要动位置
      sort(left, lPoint - 1);
      sort(lPoint + 1, right);
      
      return ;
    }
    

    现在我们来回顾上边的问题左边为基准数,为什么一定要先从右边开始?

    基准数字在左边,假设,左边先开始,找的是比基准数字大的数字:

    3, 1, 2, 6, 7
    

    如上,找到数字6的时候,左边停下了,右边开始寻找,发现直接碰撞了,变成了:

    6, 1, 2, 3, 7
    

    但是如果从右边开始就会变成

    2, 1, 3, 6, 7
    

    总结

    排序的算法还有很多,计数、基数、插入、归并、堆排序等,我们在后边会慢慢的涉及到。当然,快排的时间复杂度怎么算呢?我们想想,最差的情况,就是像冒泡一样,相邻的2个数字不停的交换,为:

    O(N^2)
    

    快排的最好时间复杂度为(为什么):

    O(NLogN)
    

    由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。

    练习

    要求对不定数量的数字进行排序,1秒之内完成。

    拿到这道题,我们关注的应当是1s之内,数字是不定量的,我们需要按照上边提供的时间复杂度来讲。假如说现在范围超级大,使用桶排序基本不可能的,因为没法申请那么大的数组,如果数字超级多,使用冒泡排序的时间复杂度又是O(N^2),但是使用快速排序却很好。代码省略了。

    本文结束。

    相关文章

      网友评论

        本文标题:算法开篇课

        本文链接:https://www.haomeiwen.com/subject/wlnmvttx.html