美文网首页
排序算法、斐波那契、尾调用优化

排序算法、斐波那契、尾调用优化

作者: 张Piers | 来源:发表于2019-12-22 10:57 被阅读0次

    时间复杂度

    我们假设计算机运行一行基础代码需要执行一次运算。

    aFunc() {
        console.log("Hello, World!");      //  需要执行 1 次
        return 0;       // 需要执行 1 次
    }
    

    那么上面这个方法需要执行 2 次运算

    bFunc(n) {
        for(let i = 0; i<n; i++) {         // 需要执行 (n + 1) 次
            console.log("Hello, World!");      // 需要执行 n 次
        }
        return 0;       // 需要执行 1 次
    }
    

    这个方法需要 (n + 1 + n + 1) = 2n + 2 次运算。

    我们把算法需要执行的运算次数用输入大小n 的函数表示,即 T(n) 。
    此时为了 估算算法需要的运行时间和简化算法分析,我们引入时间复杂度的概念。

    时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。

    所以上面的两个方法的时间复杂度是:T(n)也就是O(f(n))
    时间复杂度比较

    显然如果 f(n) = n^2,那么 T(n) = O(n^2),f(n) = O(n^3),T(n) = O(n^4) 都是成立的,但是因为第一个 f(n) 的增长速度与 T(n) 是最接近的,所以第一个是最好的选择,所以我们说这个算法的复杂度是 O(n^2) 。

    如果一个算法的执行次数是 T(n),那么只保留最高次项,同时忽略最高项的系数后得到函数 f(n),此时算法的时间复杂度就是 O(f(n))。称此为 大O推导法。

    aFunc(n) {
        for(let i = 0; i < n; i++) {         // 循环次数为 n
            console.log("Hello, World!");      // 循环体时间复杂度为 O(1)
        }
    }
    

    此时时间复杂度为 O(n × 1),即 O(n)。

    1. 对于多个循环,假设循环体的时间复杂度为 O(n),各个循环的循环次数分别是a, b, c...,则这个循环的时间复杂度为 O(n×a×b×c...)。分析的时候应该由里向外分析这些循环。
    aFunc(n) {
        for(let i = 0; i < n; i++) {         // 循环次数为 n
            for(let j = 0; j < n; j++) {       // 循环次数为 n
                console.log("Hello, World!");      // 循环体时间复杂度为 O(1)
            }
        }
    }
    

    此时时间复杂度为 O(n × n × 1),即 O(n^2)。

    1. 对于顺序执行的语句或者算法,总的时间复杂度等于其中最大的时间复杂度。
    aFunc(n) {
        // 第一部分时间复杂度为 O(n^2)
        for(let i = 0; i < n; i++) {
            for(let j = 0; j < n; j++) {
                console.log("Hello, World!");
            }
        }
        // 第二部分时间复杂度为 O(n)
        for(j = 0; j < n; j++) {
            console.log("Hello, World!");
        }
    }
    

    此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。

    1. 对于条件判断语句,总的时间复杂度等于其中 时间复杂度最大的路径 的时间复杂度。
    aFunc(n) {
        if (n >= 0) {
            // 第一条路径时间复杂度为 O(n^2)
            for(let i = 0; i < n; i++) {
                for(let j = 0; j < n; j++) {
                    console.log("输入数据大于等于零");
                }
            }
        } else {
            // 第二条路径时间复杂度为 O(n)
            for(let j = 0; j < n; j++) {
                console.log("输入数据小于零\n");
            }
        }
    }
    

    此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。

    时间复杂度分析的基本策略是:从内向外分析,从最深层开始分析。如果遇到函数调用,要深入函数进行分析。

    最后,我们来练习一下

    一. 基础题
    求该方法的时间复杂度

    aFunc(n) {
        for (int i = 0; i < n; i++) {
            for (int j = i; j < n; j++) {
                printf("Hello World\n");
            }
        }
    }
    

    参考答案:
    当 i = 0 时,内循环执行 n 次运算,当 i = 1 时,内循环执行 n - 1 次运算……当 i = n - 1 时,内循环执行 1 次运算。
    所以,执行次数 T(n) = n + (n - 1) + (n - 2)……+ 1 = n(n + 1) / 2 = n^2 / 2 + n / 2。
    根据上文说的 大O推导法 可以知道,此时时间复杂度为 O(n^2)。

    二. 进阶题
    求该方法的时间复杂度

    aFunc(n) {
        for (let i = 2; i < n; i++) {
            i *= 2;
            console.log(i);
        }
    }
    

    参考答案:
    假设循环次数为 t,则循环条件满足 2^t < n。
    可以得出,执行次数t = log(2)(n),即 T(n) = log(2)(n),可见时间复杂度为 O(log(2)(n)),即 O(log n)。

    三. 再次进阶
    求该方法的时间复杂度

    aFunc(n) {
        if (n <= 1) {
            return 1;
        } else {
            return aFunc(n - 1) + aFunc(n - 2);
        }
    }
    

    参考答案:
    显然运行次数,T(0) = T(1) = 1,同时 T(n) = T(n - 1) + T(n - 2) + 1,这里的 1 是其中的加法算一次执行。
    显然 T(n) = T(n - 1) + T(n - 2) 是一个斐波那契数列,通过归纳证明法可知,树的高度为n,一棵高度为n的满二叉树的节点个数为2^n-1,当然,上图中的树肯定不是满二叉树,但也可以看出来,该树的节点个数
    大于满二叉树节点数的一半,即(2n-1)/2,设计算次数为T(n),可知(2n-1)/2 < T(n) < 2^n-1.
    因此该算法的时间复杂度为O(2^n)

    image

    排序算法

    image

    冒泡排序(Bubble Sort)

    1. 算法描述:冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
    2. 算法实现:
      • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
      • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
      • 针对所有的元素重复以上的步骤,除了最后一个;
      • 重复步骤1~3,直到排序完成。
    function bubbleSort(arr) {
        var len = arr.length;
        console.time('冒泡排序耗时');
        for (var i = 0; i < len; i++) {
            for (var j = 0; j < len - 1 - i; j++) {
                if (arr[j] > arr[j+1]) {        //相邻元素两两对比
                    var temp = arr[j+1];        //元素交换
                    arr[j+1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        console.timeEnd('冒泡排序耗时');
        return arr;
    }
    var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
    console.log(bubbleSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
    

    选择排序(Selection Sort)

    1.算法描述:
    选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

    1. 算法实现:
    • 初始状态:无序区为R[1..n],有序区为空;
    • 第i趟排序(i=1,2,3...n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
    • n-1趟结束,数组有序化了。
    function selectionSort(arr) {
        var len = arr.length;
        var minIndex, temp;
        console.time('选择排序耗时');
        for (var i = 0; i < len - 1; i++) {
            minIndex = i;
            for (var j = i + 1; j < len; j++) {
                if (arr[j] < arr[minIndex]) {     //寻找最小的数
                    minIndex = j;                 //将最小数的索引保存
                }
            }
            temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
        console.timeEnd('选择排序耗时');
        return arr;
    }
    var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
    console.log(selectionSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
    
    image

    插入排序(Insertion Sort)

    1.算法描述:
    插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

    1. 算法实现:
    • 从第一个元素开始,该元素可以认为已经被排序;
    • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
    • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
    • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
    • 将新元素插入到该位置后;
    • 重复步骤2~5
    function insertionSort(array) {
        if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
            console.time('插入排序耗时:');
            for (var i = 1; i < array.length; i++) {
                var key = array[i];
                var j = i - 1;
                while (j >= 0 && array[j] > key) {
                    array[j + 1] = array[j];
                    j--;
                }
                array[j + 1] = key;
            }
            console.timeEnd('插入排序耗时:');
            return array;
        } else {
            return 'array is not an Array!';
        }
    }
    
    // 优化 查找插入位置时使用二分查找
    function binaryInsertionSort(array) {
        if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
            console.time('二分插入排序耗时:');
            for (var i = 1; i < array.length; i++) {
                var key = array[i], left = 0, right = i - 1;
                while (left <= right) {
                    var middle = parseInt((left + right) / 2);
                    if (key < array[middle]) {
                        right = middle - 1;
                    } else {
                        left = middle + 1;
                    }
                }
                for (var j = i - 1; j >= left; j--) {
                    array[j + 1] = array[j];
                }
                array[left] = key;
            }
            console.timeEnd('二分插入排序耗时:');
    
            return array;
        } else {
            return 'array is not an Array!';
        }
    }
    var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
    console.log(binaryInsertionSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
    
    image

    希尔排序(Shell Sort)

    1.算法描述:
    1959年Shell发明; 第一个突破O(n^2)的排序算法;是简单插入排序的改进版;它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
    希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版》的合著者Robert Sedgewick提出的。

    1. 算法实现:
    • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
    • 按增量序列个数k,对序列进行k 趟排序;
    • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
    function shellSort(arr) {
        var len = arr.length,
            temp,
            gap = 1;
        console.time('希尔排序耗时:');
        while(gap < len/5) {          //动态定义间隔序列
            gap =gap*5+1;
        }
        for (gap; gap > 0; gap = Math.floor(gap/5)) {
            for (var i = gap; i < len; i++) {
                temp = arr[i];
                for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
                    arr[j+gap] = arr[j];
                }
                arr[j+gap] = temp;
            }
        }
        console.timeEnd('希尔排序耗时:');
        return arr;
    }
    var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
    console.log(shellSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
    

    快速排序(Quick Sort)

    1.算法描述:
    快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高! 它是处理大数据最快的排序算法之一。
    快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

    1. 算法实现:
    • 从数列中挑出一个元素,称为 "基准"(pivot);
    • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
    • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
    • 详细参考
    /*方法说明:快速排序
    @param  array 待排序数组*/
    //方法一
    function quickSort(array, left, right) {
        console.time('1.快速排序耗时');
        if (Object.prototype.toString.call(array).slice(8, -1) === 'Array' && typeof left === 'number' && typeof right === 'number') {
            if (left < right) {
                var x = array[right], i = left - 1, temp;
                for (var j = left; j <= right; j++) {
                    if (array[j] <= x) {
                        i++;
                        temp = array[i];
                        array[i] = array[j];
                        array[j] = temp;
                    }
                }
                quickSort(array, left, i - 1);
                quickSort(array, i + 1, right);
            }
            console.timeEnd('1.快速排序耗时');
            return array;
        } else {
            return 'array is not an Array or left or right is not a number!';
        }
    }
    
    //方法二
    var quickSort2 = function(arr) {
        console.time('2.快速排序耗时');
        if (arr.length <= 1) { return arr; }
          var pivotIndex = Math.floor(arr.length / 2);
          var pivot = arr.splice(pivotIndex, 1)[0];
          var left = [];
          var right = [];
          for (var i = 0; i < arr.length; i++){
            if (arr[i] < pivot) {
              left.push(arr[i]);
            } else {
              right.push(arr[i]);
            }
          }
        console.timeEnd('2.快速排序耗时');
        return quickSort2(left).concat([pivot], quickSort2(right));
    };
    
    var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
    console.log(quickSort(arr,0,arr.length-1));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
    console.log(quickSort2(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
    
    image

    Q1:Array数组的flat方法实现

    Array的方法flat很多浏览器还未能实现,请写一个flat方法,实现扁平化嵌套数组,如:

    var arr1 = [1, 2, [3, 4]];
    arr1.flat(); 
    // [1, 2, 3, 4]
    

    一般解答:

    Array.prototype.flat = function() {
        var arr = [];
        this.forEach((item,idx) => {
            if(Array.isArray(item)) {
                arr = arr.concat(item.flat()); //递归去处理数组元素
            } else {
                arr.push(item);   //非数组直接push进去
            }
        })
        return arr;   //递归出口
    }
    let arr = [[2],[[2,3],[2]],3,4];
    arr.flat(); // [2, 2, 3, 2, 3, 4]
    

    牛逼解答:

    arr.prototype.flat = function() {
        this.toString().split(',').map(item=> +item )
    }
    

    Q2: 爬楼梯问题

    有一楼梯共M级,刚开始时你在第一级,若每次只能跨上一级或二级,要走上第M级,共有多少种走法?
    分析: 这个问题要倒过来看,要到达n级楼梯,只有两种方式,从(n-1)级 或 (n-2)级到达的。所以可以用递推的思想去想这题,假设有一个数组s[n], 那么s[1] = 1(由于一开始就在第一级,只有一种方法), s[2] = 1(只能从s[1]上去 没有其他方法)。
    那么就可以推出s[3] ~ s[n]了。
    下面继续模拟一下, s[3] = s[1] + s[2], 因为只能从第一级跨两步, 或者第二级跨一步。

    function cStairs(n) {
        if(n === 1 || n === 2) {
            return 1;
        } else {
            return cStairs(n-1) + cStairs(n-2)
        }
    }
    

    实际的代码千万不要这么写没有优化过的菲波切细函数,原因参考阮一峰的尾调用优化

    Q3: 二分查找

    二分查找,是在一个有序的序列里查找某一个值,与小时候玩的猜数字游戏非常像

    A: 0 ~ 100 猜一个数字
    B: 50
    A: 大了
    B: 25
    A: 对头,就是25

    思路:

    • 设定区间,low和high
    • 找出口: 找到target,返回target;
    • 否则寻找,当前次序没有找到,把区间缩小后递归
    function binaryFind(arr,target,low = 0,high = arr.length - 1) {
        const n = Math.floor((low+high) /2);
        const cur = arr[n];
        if(cur === target) {
            return `找到了${target},在第${n+1}个`;
        } else if(cur > target) {
            return binaryFind(arr,target,low, n-1);
        } else if (cur < target) {
            return binaryFind(arr,target,n+1,high);
        }
        return -1;
    }
    
    // 优化
    function binaryFind(arr, target) {
        var low = 0,
            high = arr.length - 1,
            mid;
        while (low <= high) {
            mid = Math.floor((low + high) / 2);
            if (target === arr[mid]) {
                return `找到了${target},在第${mid + 1}个`
            }
            if (target > arr[mid]) {
                low = mid + 1;
            } else if (target < arr[mid]) {
                high = mid - 1;
            }
        }
        return -1
    }
    

    在我们的某个项目里抠出来的一段代码

    
    for (let index = 0; index < this.list.length; index++) {
        let thisStart=this.list[index].StartDateTime;
        let thisEnd=this.list[index].EndDateTime;
        for (let nextIndex =index+1; nextIndex < this.list.length; nextIndex++) {
            let nextStart=this.list[nextIndex].StartDateTime;
            let nextEnd=this.list[nextIndex].EndDateTime;
            let thisStartBetween=moment(thisStart).isBetween(moment(nextStart), moment(nextEnd));
            let thisEndBetween=moment(thisEnd).isBetween(moment(nextStart), moment(nextEnd));
            let nextStartBetween=moment(nextStart).isBetween(moment(thisStart), moment(thisEnd));
            let nextEndBetween=moment(nextEnd).isBetween(moment(thisStart), moment(thisEnd));
            if (thisStartBetween||thisEndBetween||nextStartBetween||nextEndBetween||thisStart===nextStart||thisEnd===nextEnd) {
                Toptip.error(i18n.t('leaveExceptionFrom', {ref:index+1})+i18n.t('leaveExceptionTo', {ref:nextIndex+1}));
                return;
            }
        }
    }
    

    优化1: 无重叠区间

    let list = [
        {
            startDate: '2019-11-12 09:00',
            endData: '2019-11-12 12:00'
        },
        {
            startDate: '2019-11-13 09:00',
            endData: '2019-11-13 12:00'
        },
        {
            startDate: '2019-11-13 22:00',
            endData: '2019-11-14 08:00'
        },
        {
            startDate: '2019-11-14 09:00',
            endData: '2019-11-14 17:00'
        },
        {
            startDate: '2019-11-11 22:00',
            endData: '2019-11-12 10:00'
        },
        {
            startDate: '2019-11-14 10:00',
            endData: '2019-11-14 12:00'
        },
    ];
    let startArr = [];
    let endArr = [];
    list.forEach(item => {
        startArr.push(item.startDate);
        endArr.push(item.endData);
    });
    startArr = startArr.sort();
    endArr = endArr.sort();
    startArr.forEach((obj, index) => {
        if(startArr[index+1] < endArr[index]) {
            console.log('有重叠');
            return false;
        }
    });
    

    优化2: 合并区间
    ),然后比较区间差总和

    let merge=(intervals) => {
        intervals.sort((a,b)=>{
            //先排序
            if(a[0]!=b[0]){return a[0]-b[0]}
            return a[1]-b[1]
        })
        let len=intervals.length
        let res=[],start,end
        for(let i=0;i<len;i++){
            let s=intervals[i][0],e=intervals[i][1]
            if(!start) {start=s,end=e}
            else if(s<=end){end=Math.max(end,e)}
            else{
                let part=[start,end]
                res.push(part)
                start=s
                end=e
            }
        }
        if(start){
            let part=[start,end]
            res.push(part)
        }
        return res
    }
    let arr=[
    [1,3],[2,6],[8,10],[9,11]]
    
    console.log(merge(arr)) //[1,6],[8,11]
    

    相关文章

      网友评论

          本文标题:排序算法、斐波那契、尾调用优化

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