《算法图解》读书笔记一

146 阅读2分钟

第一章 算法简介

一、二分查找

从一个有序的元素列表中查找元素;每次取中间数并判断与目标元素的大小,排除了一半的数据,再从剩下列表中不断与新的中间数比较,直到找到目标元素。

function numSearch(arr, target) {
    let low = 0,
        high = arr.length - 1,
        mid, midNum
    while (low <= high) {
        mid = Math.ceil((low + high) / 2)
        midNum = arr[mid]
        if (target == midNum) {
            return mid
        } else if (target < midNum) {
            high = mid - 1
        } else {
            low = mid + 1
        }
    }
    return null
};

二、大 O 表示法

表示算法运行时间随列表增长的增速,以增速为角度度量而不是秒。例如,假设列表包含n个元素。简单查找需要检查每个元素,运行时间为O(n)(线性时间);而使用二分查找需要检查行log n次,运行时间为O(log n)(对数时间)。

大 O 表示法指出了最糟情况下的运行时间。

第二章 选择排序

一、数组和链表

  • 数组:存储数据的内存空间是连续的,每个元素都具备索引,索引从0开始;(支持随机访问)
  • 链表:每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起;(支持顺序访问)

顺序访问意味着从第一个元素开始逐个地读取元素,随机访问意味着可直接跳到第十个元素。

操作运行时间

数组链表
读取O(1)O(n)
插入O(n)O(1)
删除O(n)O(1)

二、选择排序

循环遍历列表,找出最大的数,并将该数添加到一个新列表中,直到所有数都排列为止。运行时间为O(n*n)

function findSmallest(arr) { // 寻找数组中最小的数
    let smallest = arr[0] // 存储最小的值
    let smallest_index = 0 // 存储最小元素的索引
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < smallest) {
            smallest = arr[i]
            smallest_index = i
        }
    }
    return smallest_index
}

function selectionSort(arr) {// 对数组进行排序
    let newArr = [],smallest
    for(let i = 0; i < arr.length; i++){
        smallest = findSmallest(arr) // 找出数组中最小的元素,并将其加入到新数组中
        newArr.push(arr[smallest])
        arr.splice(smallest,1)
    }
    return newArr
}

第三章 递归

一、递归

递归就是函数调用自己,让解决方案更清晰。

编写递归函数时,必须告诉它何时停止递归。每个递归函数都有两部分:基线条件和递归条件。递归条件指的是函数调用自己,而基线条件则指的是函数不再调用自己,从而避免形成无限循环。

// 递归
function countdown(i){
    if(i<=0){// 基线条件
        return
    }else{// 递归条件
        countdown(i-1) 
    }
}

二、栈

栈是一种简单的数据结构,具有入栈和出栈的操作,操作的都是最上面的元素,计算机在内部使用被称为调用栈的栈。 使用栈虽然很方便,但存储详尽的信息可能占用大量的内存。每个函数调用都要占用一定的内存,如果栈很高,就意味着计算机存储了大量函数调用的信息。

第四章 快速排序

一、分而治之

分而治之(divide and conquer,D&C)是一种著名的递归式问题解决方法,通过不断缩小问题的规模来解决问题。因为是递归式,因此也会包含基线条件和递归条件。

D&C并非可用于解决问题的算法,而是一种解决问题的思路。

以计算数组之和为例:

image.png

二、快速排序使用

快速排序使用D&C解决,基线条件为数组为空或只包含一个元素。

  • 当数组为空或长度为1,只需原样返回数组——根本就不用排序;
  • 当数组长度为2,判断两个元素的大小并交换位置;
  • 当数组长度为3,将数组分解直到满足基线条件。

快速排序的工作原理:

  1. 从数组中选择一个元素,这个元素被称为基准值;
  2. 分区——找出比基准值小的元素以及比基准值大的元素,分成两个子数组和一个基准值;
  3. 采用递归对子数组进行快速排序;
  4. 合并结果,得到有序数组。
function quicksort(arr){
    let len = arr.length
    if(len<2){
        return arr
    }else{
        let pivot = arr[0]
        let less=[],greater=[]
        for(let i=1;i<len;i++){
            if(arr[i]<pivot){
                less.push(arr[i])
            }else{
                greater.push(arr[i])
            }
        }
        return quicksort(less).concat([pivot],quicksort(greater))
    }
}

若快速排序在平均情况下的运行时间为O(n log n),在最糟情况下,其运行时间为O(n*n)。

平均情况和最糟情况

快速排序的性能高度依赖于你选择的基准值。

image.png

这个数组并没有被分成两半;相反,其中一个子数组始终为空,这导致调用栈非常长,属于最糟情况;运行时间为O(n*n)

image.png

这个数组每次都将数组分成两半,所以不需要那么多递归调用,很快就到达了基线条件,因此调用栈短得多,属于最佳情况;运行时间为O(log n*n)

最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(n log n)。

第五章 散列表

一、散列函数 && 散列表

散列函数:“将输入映射到数字”,将同样的输入映射到相同的索引,将不同的输入映射到不同的索引。散列函数知道数组有多大,只返回有效的索引。

散列表:结合使用散列函数和数组创建的一种数据结构,数组和链表都被直接映射到内存,但散列表更复杂,它使用散列函数来确定元素的存储位置。散列表由键和值组成。

Python提供的散列表实现为字典,你可使用函数dict来创建散列表;在JavaScript则可通过包含多个属性的对象来代表散列表。

image.png

二、冲突

冲突是指给两个键分配的位置相同;

处理冲突最简单的办法:如果两个键映射到了同一个位置,就在这个位置存储一个链表。

而最理想的情况是,散列函数将键均匀地映射到散列表的不同位置,好的散列函数很少导致冲突。否则,如果散列表存储的链表很长,散列表的速度将急剧下降。

在平均情况下,散列表执行各种操作的时间都为O(1),即不管数组多大,从中获取一个元素所需的时间都是相同的。

image.png

三、填装因子

填装因子 = 散列表占位数 / 散列表位置总数,填装因子度量的是散列表中有多少位置是空的。

image.png

image.png

假设你要在散列表中存储100种商品的价格,而该散列表包含100个位置。那么在最佳情况下, 每个商品都将有自己的位置,这个散列表的填装因子为1。一般来说,一旦填装因子>0.7就需要调整散列表长度,通常将数组增长一倍。

需要使用函数hash将所有的元素都插入到这个新的散列表中,良好的散列函数让数组中的值呈均匀分布。而良好的散列函数的实现,可了解SHA函数。