算法初体验

2,087 阅读6分钟

我们知道程序由数据结构和算法组成的。其中,数据结构表示数据的组织形式,基本的数据结构包括数组、链表、栈、队列、树、哈希表、图、堆等。而算法表示对数据结构中的数据进行处理的方式或过程,换句话说,就是解决问题的方法。它们俩之间的关系:数据结构为算法服务,很多算法依赖于特定的数据结构,但不是全部算法,算法可以和数据结构没有关系。本期我们就来聊一聊算法。

学习算法的重要性

在介绍具体算法之前,我先谈一下个人对学习算法的初心。我的初心无非有两点:一,BAT等互联网公司招聘面试时要问算法知识,如果想要进入互联网公司,我就必须学好算法;二,通过学习算法提升个人开发的基本功,这样一来,对于不同场景我就可以正确选择对应的数据结构和算法,使得程序更健壮,提高程序的运行效率。

应用领域

目前计算机各个细分领域涉及到不同的算法。比如说搜索引擎,平时我们使用google、百度等浏览器,只要我们输入一个关键字,浏览器就会快速地返回相关的集合,这个集合的背后就隐藏着许多算法。如果没有这些算法,我们是不可能这么快速地得到想要的结果。再比如说人工智能,通过计算模型算法实现人体识别、语音识别等各应用场景。

算法分析

上文我们已经介绍到算法就是解决问题的方法,而对于同一个问题,可能存在不同的解决方法。因此,为了衡量一个算法的优劣,提出了时间复杂度空间复杂度这两个概念。

时间复杂度

一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数f(n),算法的时间度量记为 T(n) = O(f(n)),它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度。

空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量。

排序算法

根据时间复杂度我们大体可以将排序算法分为两类,一类是以选择排序为代表的O(n^2)的算法,另一类是以快速排序为代表的O(nlogn)的算法。看到这里我们不禁会问:既然有O(nlogn)的排序算法,那些O(n^2)的算法还有存在的必要吗?要回答这个问题,先来看下O(n^2)的排序算法的特点:首先,它相对是比较基础的,编码简单,易于实现,在一些特定场景下O(n^2)更适合 ,譬如在机器语言中O(n^2)更容易实现;其次,简单的排序算法思路衍生出复杂的排序算法,比如说希尔排序是对插入排序的优化;最后,对于一些简单的算法,由于它们本身的一些性质,可以被用作改进更复杂排序算法的子过程中。基于此,本文O(n^2)排序算法中两个代表性的算法即选择算法和插入算法。

image.png

选择排序

思想:在整个待排序数组里找到最小的值,然后和待排序中的第一个元素进行交换,接着在剩下的元素里找到最小的元素,接着将它和待排序中的第一个元素进行交换,以此类推。为了加深大家的理解,举个具体例子,对8、6、2、3、1、5、7、4进行升序排序。

image.png

选择排序的Java语言实现如下:

   /**
     * 思路:每次从待选数组中选择一个最小元素,然后和对应位置交换位置
     * @param arr
     * @param n
     */
    public void sort(int[] arr, int n) {
        for(int i=0;i<n;i++) {
            // 1. 寻找[i,n)区间里的最小元素
            int minIndex = i;
            for(int j=i+1;j<n;j++ ) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // 2. 交换位置
            this.swap(arr,i,minIndex);

        }
    }

插入排序

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

image.png
插入排序的Java语言实现如下:

 public void sort(Comparable[] arr){
        int n = arr.length;
        for (int i = 0; i < n; i++) {
            // 寻找元素arr[i]合适的插入位置
            for( int j = i; j > 0 && arr[j].compareTo(arr[j-1]) < 0 ; j--)
                swap(arr, j, j-1);
        }
    }

通过比较选择排序和插入排序的代码实现,我们可以发现一旦有部分排序好之后,新插入一个数如果比排好序最大值还要大,则不用再和其他数字比较,减少了比较次数。但是,我们应该注意到插入排序在每次遍历的时候都需要进行交换操作,这个交换操作包含三次赋值操作,导致插入排序的时间要比选择排序的时间更长。针对这个问题,我们的先辈们想到了一个方法:先将待比较元素复制一份,然后依次和有序数组中的元素进行比较,如果比有序数组中的元素小,则将有序数组中的元素覆盖待比较元素,以此类推。如下图所示,首先我们将元素6复制一份,接着验证元素6是否应当放在当前位置,通过比较6和它之前的元素大小,发现元素8应该放在元素6的位置上,因此将元素8覆盖元素6,然后我们考查元素6是否应该放在前一个元素位置上,此时,由于元素8在第0个位置上我们就不用比较直接覆盖。它的Java代码实现如下:

image.png

 for (int i = 0; i < n; i++) {
            // 寻找元素arr[i]合适的插入位置
            Comparable e = arr[i];
            int j = i;
            for( ; j > 0 && arr[j-1].compareTo(e) > 0 ; j--)
                arr[j] = arr[j-1];
            arr[j] = e;
        }

这样一来,内循环只需要进行一次赋值操作,效率得到了大大优化,不仅超过了选择排序,而且在待排序数组是有序的情况下,时间复杂度可以达到O(n)


image

欢迎关注微信公众号:木可大大,所有文章都将同步在公众号上。