《图解算法》笔记 上

297 阅读4分钟

这是阅读了著名的《图解算法》记得一些笔记,只记录到了第4章,应该很快就会有下 不过话说后面的算法,比如dijkstra算法,这些前端面试好像也不会考,看了看牛客上的一些面经一般好像就到翻转链表这种级别(

第一章 算法简介

1.2 二分查找

二分查找只能用在 有序的数据结构中,否则没有任何作用

二分查找的时间复杂度为O(logn),因为二分所以取2 ** x = n,此时x为logn

在js中一个典型的二分搜索如下:

//list应为有序数组,从小到大,如 list = [1,2,3,4,5]
const binarySearch = (list, x)  => {
    let high = list.length - 1 ;
    let low = 0;
    while(low <= high){
        let mid = Math.ceil((high + low) / 2);
        let guess = list[mid];
        if (guess === x) {
            return mid;
        }
        if(guess > x) {
            high = mid - 1;
        }else {
            low = mid + 1;
        }
    }
    //兜底,没有找到元素
    return undefined;
};

练习

1.1 假设有一个包含128个名字的有序列表,你要使用二分查找在其中查找一个名字,请 问最多需要几步才能找到?

//2 ** 7 =128 所以是7次

1.2 上面列表的长度翻倍后,最多需要几步?

//2 ** 8 =128*2 = 256 所以是八次

1.3 大O表示法

练习

使用大O表示法给出下述各种情形的运行时间。 1.3 在电话簿中根据名字查找电话号码。

//如果使用简单则为O(n),二分则为O(logn)

1.4 在电话簿中根据电话号码找人。(提示:你必须查找整个电话簿。)

//必须找遍整个电话薄就是O(n)

1.5 阅读电话簿中每个人的电话号码。

//没太看懂,如果说每个电话是m个字符,有n个人,那么就是O(n*m)

1.6 阅读电话簿中姓名以A打头的人的电话号码。这个问题比较棘手,它涉及第4章的概 念。答案可能让你感到惊讶!

//第四章讲快速排序,这里我不明白他要讲啥

旅行商问题

给定n个点,求n个点两两连线时,总线段长度最短的情况,这是一个典型的O(n!)的问题,它是一个 NP 完全问题。有一些算法可以近似这个问题,比如退火算法,遗传算法,蚁群算法等

第二章 选择排序

2.2 链表与数组

链表与数组都是有序的数据结构,什么时候用哪个呢? 由于数组的元素是连续的,所以在增加元素时,原来的内存空间可能已经没有位置了,这时就需要全部移到另一个地方去,而链表仅仅记录下一个元素的地址,并不是连续的,所以不要移动(电影院一群人找座位)

但是链表查找特定元素时,必须从头开始查找,因为无法直接通过地址等访问到那个值,而数组可以直接通过下标访问

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

2.3 选择排序

选择排序就是读取数组中每一项的值然后进行排序,时间复杂度是O(n^2)

用js实现的典型的选择排序如下:

const selectionSort = (arr) => {
  let newArr = [];
  let len = arr.length;

  for (let i = 0; i < len; i++) {
      let smallest = arr[0];
      let smallest_index = 0;
      
      for(let j = 1; j < len - 1; j++) {
      	if (arr[j] < smallest) {
            smallest = arr[j];
            smallest_index = j;
        }
    }
    // 把最小元素切下push到arr开头
    newArr.push(arr.splice(smallest_index,1)[0]);
  }
  return newArr;
}

第三章 递归

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

假设我要证明我能爬到梯子的最上面。递归条件是这样的:如果我站在一个横档上,就能将脚放到下一个横档上。换言之,如果我站在第二个横档上,就能爬到第三个横档。这就是归纳条件。而基线条件是这样的,即我已经站在第一个横档上。因此,通过每次爬一个横档,我就能爬到梯子最顶端。

调用栈(call stack)

调用另一个函数时,当前函数暂停并处于未完成状态

最外层的函数是最早被调用,但最后执行完的,这是一个先进后出的结构,所以是个栈

程序内部处理函数的结构就是一个调用栈

第四章 快速排序

分而治之(D&C)

divide and conquer提供了一种思想,将问题简化为不能再简单的基线条件,然后反复的缩小问题的规模直到成为基线条件

比如给定长方形划分为尽可能大的正方形

而编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。

比如计算一个数组值的和

// 循环
const sum = (list) => {
    if(list.length === 0) {return 0;}
    let total = 0;
    for(i in list) {
        total += i;
    }
}
// 递归
const sum = (list) => {
    if(list.length === 0) {
        return 0;
    }
    if(list.length === 1) {
        return list[0];
    }else {
        // 把最后一项加到倒数第二项上
        let end = list.pop();
        list[list.length - 1] += end;
        // console.log(list);
        sum(list);
    }
    // 注意注意注意一定要在递归函数最后加上return,不然返回undefined
    return list[0];
};

快速排序

快速排序采用了分而治之的思想,使用递归来解决问题

具体做法为:

(1) 选择基准值(可以简单的设置为第一项)。 (2) 将数组分成两个子数组:小于基准值的元素(左数组)和大于基准值的元素(右数组)。 (3) 对这两个子数组进行快速排序。

const quicksort = (arr) => {
    if (arr.length == 0) return [];
    let left = [];
    let right = [];
    let pivot = arr[0];
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < pivot) {
           left.push(arr[i]);
        } else {
           right.push(arr[i]);
        }
    }
    // 对左数组和右数组分别进行快速排序,并且将它们与基准值拼接
    return quicksort(left).concat(pivot, quicksort(right));
}
// [2,5,8,4,1]
// 1. l=[1] p=2 r=[5,8,4]
// 2. 1.concat(2, quicksort([5,8,4]))
// 3. 1.concat(2, 4.concat(5,quicksort[8])))
// 4. 1.concat(2, 4.concat(5,8)) -> [1,2,4,5,8]

快速排序的时间复杂度最坏情况下是O(n^2),想象选定一个有序数组arr[0]作为基准值,对每个元素都进行排列,一共调用了n次,每层(调用栈层)使用了n个元素,所以为O(n*n)

而最佳情况时,应该选定中间值作为基准值,对一个正序数组进行正序排列,这时每层调用n个元素,但是层为logn,所以为O(nlogn)

而对于无序数组,最佳情况就是平均情况

注意:快速排序在一般情况下比合并排序快,这涉及到语言原始的结构(即虽然都是O( C * nlogn),但是其实隐去的常量C快排更低),记住就好