这是阅读了著名的《图解算法》记得一些笔记,只记录到了第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快排更低),记住就好