算法笔记(上)

111 阅读6分钟

数据结构与算法

这是9月看了一些数据结构相关的课程记录的算法笔记,语言均采用js,部分内容还没有写完,会不时修改

1.栈

1.1操作方法

在js中用数组来模拟一个栈

可以用push和pop来模拟栈的先进后出(LIFO)特性

1.2使用场景

①十进制转二进制

​ 十进制转二进制实际上是通过不断地除2取余数完成的,最后除的余数作为第一位(先进后出)

②括号匹配

​ 左括号入栈右括号出栈,以及可以设置不同的类型匹配

③函数调用堆栈

​ 函数层级调用中,先调用的函数是最后结束的(先进后出),必须等到子层级中的函数调用完,外层的才能结束

1.3相关题目

2.队列

2.1操作方法

2.2使用场景

2.3相关题目

20.有效的括号

3.链表

链表中的元素存储不是连续的,而且对于一个元素来说只能访问下一个元素而不能访问上一个元素

就像是间谍,采用单线沟通,除了上级没有人能证明你是卧底

3.1操作方法

使用object来模拟

val存储当前节点的值,next存储下一个节点的引用

从头部节点(第一个节点)开始算为一个完整的链表

3.2使用场景

①原型链

原型链的节点是各种原型对象(Funtion.prototype, Object.prototype等)

原型链通过 __proto__来连接各种原型对象(相当于next)

比如一个function的原型链 : function -> Function.prototpye -> Object.protoytpe -> null

可以这样检验

const func = () => {};
func.__proto__.__proto__ === Object.prototype // true

如果A沿着原型链可以找到B.prototype 那么 A instanceof B 为true

这样我们就可以手写instanceof方法

const instanceof = (A, B) => {
    //指定指针
    let point = A;
    //遍历链表(原型链)
    while(point) {
        if(point === B.prototype) {
            return true;
        }
        point = point.__proto__
    }
    return false;
}

如果在A对象上找不到x属性,就会沿着原型链寻找x属性

const arr = [];
Object.prototpye.x = 'x'
console.log(arr.x) //'x'

②使用链表获取json节点值

const json = {
    a: {b: {c: 1}},
    d: {e: 2}
};
let path = ['a', 'b', 'c'];
let p = json;
path.forEach(k => {
    p = p[k];
})
console.log(p) //p = 1

3.3相关题目

349.两个数组的交集

4.集合

集合是一种无序且唯一的数据结构

4.1操作方法

ES6自带了集合的数据结构Set,可以直接使用

用has判断集合是否有某一元素,同时可以通过keys和values进行迭代访问所有值(在set中,keys和values是一样的)

用set添加元素(多次添加重复元素不会报错但是会后续会无效,这里对于属性值相同的但是内存不同的对象不判定为重复)

用size访问集合的尺寸(有几个元素就有多大)

set和array互相转换方法:

//set -> array
const arr1 = Array.from(set1);
const arr1 = [...set1];
//array -> set
const set1 = new Set(arr1);

4.2使用场景

①去重

const arr = [1,1,2,2,3];
//去重用数组转集合,利用集合唯一性
const arr2 = [...new Ser(arr)];

②求交集

const set1 = new Set([1,2,3,5]);
const set2 = new Set([1,2,3,4]);
//求交集需要集合转数组filter
const intersection = new Set([...set1].filter(item => set2.has(item)));

顺便提一下求差集的方法(就是加一个非)

const intersection = new Set([...set1].filter(item => !set2.has(item)));

5.字典

字典是一种存储唯一值的数据结构,用键值对形式存储

5.1操作方法

ES6中自带了字典数据结构Map

const m = new Map();
//增&查
m.set('key1', 'value1');
m.get('key1') //'value1'
//删
m.delete('key1')
m.get('key1') // undefined
m.clear() //删除所有
//改
m.set('key1', 'value2')

5.2使用场景

5.3相关题目

349.两个数组的交集,这题用map比用set要好一些

20.有效的括号,这题用map简洁很多(在规则匹配上)

1.两数之和,用map记录数据

3.无重复字符的最长字符串,用双指针滑动窗口,用map记录值和下标(很重要)

76.最小覆盖子串,同样是双指针滑动窗口,不过需要动态的匹配和移动,不愧是困难

6.树

树是一种分层数据的抽象模型

6.1操作方法

js中没有树,但是可以用OBject和Array来构建树

const tree = {
    value: '中国',
    label: 'china',
    children: [
        {
           	value: '福建',
            label: 'Fujian', 
            children: [
                {
                    value: '泉州',
            		label: 'Quanzhou', 
                }
            ]
        }
    ]
}

树的常用操作:深度/广度优先遍历,二叉树先中后序遍历

②深度/广度优先遍历

深度优先遍历就是尽可能深的搜索树的分支

深度优先遍历的算法:1.访问根节点 2.对根节点的children挨个进行深度优先遍历 3.把当前节点作为根节点,重复以上过程

const tree = {
    val: 'a',
    children: [{ val: 'b',children: [{val: 'd',children: []},{val: 'e',children: []},
               { val: 'c',children: [{val: 'f',children: []},{val: 'g',children: []}]
               };
                                     
const dfs = (root) => {
                console.log(root.val);
                root.children.forEach(dfs);
               };

广度优先遍历则是先访问离根节点最近的节点

广度优先遍历的算法:1.新建队列并把根节点入队 2.把对头出队并访问 3.把对头的children挨个入队 4.重复2,3步 直到队列为空

const bfs = (root) => {
    const queue = [root];
    while(queue) {
        const n = queue.shift();
        console.log(n.val);
        n.children.forEach((child) => {queue.push(child)});
    }
};

②二叉树

二叉树是指书中每个节点都只能有两个子节点

在js中用Object来模拟二叉树

const binaryTree = {
    val: 1,
    left: {
        val: 2,
        left: 3,
        right: 4
    },
    right: {
        val: 5,
        left: 6,
        right: 7
    }
}

先序遍历(根左右): 1.访问根节点 2.对根节点的左子树进行先序遍历 3. 对根节点的右子树进行先序遍历

const preorder = (root) => {
    if(!root) { return; }
    console.log(root.val);
    preorder(root.left);
    preorder(root.right);
}

中序遍历(左根右): 1.对根节点的左子树进行中序遍历 2.访问根节点 3.对根节点的右子树进行中序遍历

const inorder =(root) => {
    if(!root) { return; }
    inorder(root.left);
    console.log(root.val);
    inorder(root.right);
}

后序遍历(左右根):1.对根节点的左子树进行中序遍历 2.对根节点的右子树进行中序遍历 3.访问根节点

const postorder =(root) => {
    if(!root) { return; }
    postorder(root.left);
    postorder(root.right);
    console.log(root.val);
}

当然,以上三种遍历都可以采用非递归的方法实现

整体思路就是用stack去模拟函数调用的堆栈顺序从而实现

const preorder = (root) => {
    if(!root) { return; }
    //console.log(root.val);
    const stack = [root];
    while(stack.length) {
        const n = stack.pop();
        console.log(n.val);
        //preorder(root.left);
        //preorder(root.right);
        //栈先进后出,所以right在left前入栈
        if(n.right) {stack.push(n.right)};
        if(n.left) {stack.push(n.left)};
    }
};

中序遍历和后序遍历也是一样的思路,后序遍历就是先序遍历的算法的逆序(入栈顺序相反)

const inorder = (root) => {
    if(!root) { return; }
    const stack =[];
    let p = root;
    while(stack.length || p) {
        while(p) {
            stack.push(p);
            p = p.left;
    	};
        const n =stack.pop();
        console.log(n.val);
        p = n.right;
    }
};
const preorder = (root) => {
    if(!root) { return; }
    //console.log(root.val);
    const stack = [root];
    //配合入栈实现倒序
    const outputstack = [];
    while(stack.length) {
        const n = stack.pop();
        outputstack.push(n.val);
        // 同样倒序
        if(n.left) {stack.push(n.left)};
        if(n.right) {stack.push(n.right)};
    }
    while(outputstack.length) {
        const n = outputstack.pop();
        outputstack.push(n.val);
    }
};

6.2使用场景

①DOM树

②级联选择

③树形控件(目录结构)

6.3相关题目

104.二叉树的最大深度(深度优先遍历)

111.二叉树的最小深度(广度优先遍历)