浅谈前端常用数据结构和算法(一)

本文深入探讨了数据结构中的线性结构,包括数组和链表的特性与优缺点,以及链表的逆置。接着介绍了排序算法如冒泡排序和快速排序的基本思想。接着讨论了栈和队列,以及二维数据结构。最后,详细阐述了二叉树的遍历、构建和搜索,以及最小生成树的普利姆和克鲁斯卡尔算法。
摘要由CSDN通过智能技术生成

数据结构与算法

  • 数据结构与算法有什么关系?
    可以容纳数据的结构称为数据结构。
    算法是用来对数据结构进行处理的方法。
    数据结构是静态的,算法是动态的。

线性数据结构(一维数据结构)

线性数据结构强调存储与顺序。

数组

数组特性:
1. 存储在物理空间上是连续的。
2. 底层的数组长度是不变的。(数组定长)
3. 数组的变量指向数组第一个元素的位置。(方括号表示存储地址的偏移量【操作系统小知识: 通过偏移查询数据性能最好】)

优点: 查询性能好。指定查询某个位置。
缺点:
1. 因为空间必须连续,若数组比较大,当系统碎片空间较多的时候,容易存不下。
2. 因为数组的长度固定,所以数组的内容难以被添加和删除。

链表

传递链表,必须传递链表的根节点。(都指单链表)
每一个节点都认为自己是根节点。

链表的特性:
1. 空间上不是连续的。
2. 每存放一个值,都要多开销一个引用空间。

优点:
1. 只要内存足够大,就能存的下,不用担心空间碎片的问题。
2. 链表的添加和删除非常的容易。

缺点:
1. 查询的速度慢,(指的查某个位置)。
2. 链表每一个节点都需要创建一个指向next的引用,浪费一些空间。当节点内数据越多时,这部分多开销的内存影响越少。

链表的逆置
function Node(value){
    this.value=value;
    this.next=null;
}

let node1 = new Node(1),
    node2 = new Node(2),
    node3 = new Node(3),
    node4 = new Node(4),
    node5 = new Node(5);

node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
node5.next = null;

function reverseLink(root){
    if(root.next.next == null){
        root.next.next = root;
        return root.next;
    }else{
        let result = reverseLink(root.next);//在操作之前进行递归,建立函数执行栈,让其从后往前运行
        root.next.next = root;
        root.next = null;//此处置空是为了保证node1.next指向null
        return result;//返回结构一直为node5,保持结果返回
    }
}

console.log(reverseLink(node1));

双向链表

没有算法

优点:无论给出任何节点,都能对整个链表进行遍历。
缺点:多费一个引用空间,而且构建双向链表比较复杂。

function Node(value){
    this.value = value;
    this.prev = null;
    this.next = null;
}
let node1 = Node(1),
    node2 = Node(2),
    node3 = Node(3),
    node4 = Node(4),
    node5 = Node(5);
node1.prev = null;
node1.next = node2;
node2.prev = node1;
node2.next = node3;
node3.prev = node2;
node3.next = node4;
node4.prev = node3;
node4.next = node5;
node5.prev = node4;
node5.next = null;

线性数据结构的遍历

遍历:将一个集合中的每一个元素进行获取并查看

(算法题必须考虑严谨性判断)

递归遍历必须有出口,一般先找递归出口再递归

排序

排序不是比较大小。
排序的本质是比较和交换。
任何一种排序算法都没有优劣之分,只有是否适合的场景。

冒泡排序

选择排序

选择排序,内层循环,每一圈选出一个最大的,然后放在后面

快速排序


//【简单快排】优化后的版本,不是性能最好的版本,便于记忆
function quickSort(arr){
    if(arr == null || arr.length == 0) return [];
    let lead = arr[0];
    let left = [],
        right = [];
    for(let i = 0;i < arr.length; i++){
        if(arr[i] < lead) left.push(arr[i]);
        else right.push(arr[i]);
    }
    left = quickSort(left);
    right = quickSort(right);
    left.push(lead);
    return left.concat(right);
}

//【标椎快排】
function swap(arr, a, b){
    arr[a]=[arr[b]][(arr[b] = arr[a], 0)];
}
function quickSort2(arr, begin, end){//begin和end为下标
    if(begin >= end -1) return ;
    let left = begin,
        right = end -1;
    do{
        //左指针从左向右查找比arr[begin]大的数,如果有则跳出循环进行交换,如果没有则继续向右查找
        do{left++;}while(left < right && arr[left] < arr[begin]);
        //当左指针跳出循环后,右指针从右向左查找比arr[begin]小的数,如果有则跳出循环,否则继续
        do{right--;}while(left > right && arr[right] > arr[begin]);
        //左指针找到第一个比arr[begin]大的数,右指针找到第一个比其小的数,相互交换位置
        if(left < right) swap(arr, left, right);
    }while(left < right);
//一圈循环完毕,左边都比arr[begin]小
    let swapPoint = left == right ? right - 1 : right;
    swap(arr, begin, swapPoint);//将中间数据和begin位置数据互换
    quickSort2(arr, begin, swapPoint);
    quickSort2(arr, swapPoint + 1, end);
}
function quickSort(arr){
    quickSort(arr, 0, arr.length);
}

栈和队列

栈:先进后出
队列:先进先出

二维数据结构

二维数组

二维拓扑结构(图)

树结构(有向无环图)

树形结构有一个根节点
树形结构没有回路

叶子节点:下边没有其他节点
节点:既不是根节点,也不是叶子节点
子节点:某个节点下面的节点
树的度:这棵树有最多叉的节点有多少个叉,这棵树的度就为多少
树的深度:树最深有几层

二叉树

树的度最多为2的树形结构

满二叉树:
(1) 所有的叶子节点都在最底层
(2) 每个非叶子节点都有两个子节点

完全二叉树:
国内定义:
(1) 叶子节点都在最后一层或倒数第二层
(2) 叶子节点都向左聚拢
国际定义:
(1) 叶子节点都在最后一层或倒数第二层
(2) 如果有叶子节点,就必然有两个叶子节点

在二叉树中,每个节点都认为自己是根节点
子树:二叉树中,每一个节点或叶子节点,都是一颗子树的根节点
左子树、右子树

二叉树的遍历

传递二叉树要传根节点。
前序遍历:(先根次序遍历) 先打印当前的,再打印左边的,再打印右边的
中序遍历:(中根次序遍历) 先打印左边的,再打印当前的,再打印右边的
后序遍历:(后跟次序遍历) 先打印左边的,再打印右边的,再打印当前的

//二叉树结构
function Node(value){
    this.value = value;
    this.left = null;
    this.right = null;
}
let a = new Node("a"),
    b = new Node("b"),
    c = new Node("c"),
    d = new Node("d"),
    e = new Node("e"),
    f = new Node("f"),
    g = new Node("g");
a.left = c;
a.right = b;
c.left = f;
c.right = g;
b.left = d;
b.right = e;
  1. 前序遍历
//前序遍历
function f1(root){
    if(root == null) return;
    console.log(root.value);
    f1(root.left);
    f1(root.right);
}
f1(a);
  1. 中序遍历
//中序遍历
function f1(root){
    if(root == null) return;
    f1(root.left);
    console.log(root.value);
    f1(root.right);
}
  1. 后序遍历
 //后序遍历
function f1(root){
    if(root == null) return;
    f1(root.left);
    f1(root.right);
    console.log(root.value);
}
  1. 给出二叉树,写出前序中序后序的遍历
  2. 写出前序中序后序遍历的代码
  3. 给出前序中序还原二叉树,要求写出后序遍历
  4. 给出后序中序还原二叉树,要求写出前序遍历
  5. 代码实现前序中序还原二叉树
  6. 代码实现后序中序还原二叉树

心得:
前序遍历、后序遍历找根节点;
中序遍历找根节点的左右子树或子节点

前序中序还原二叉树代码

var front = ['a', 'c', 'f', 'g', 'b', 'd', 'e'];
var middle = ['f', 'c', 'g', 'a', 'd', 'b', 'e'];
function recover(front, middle){
    if(front == null || middle == null || front.length == 0 || middle == 0 || front.length !== middle.length) return null;
    let root = new Node(front[0]);
    let rootIndex = middle.indexOf(root.value);//找到根节点在中序遍历中的位置
    let fl = front.slice(1, rootIndex + 1),//前序遍历的左子树
        fr = front.slice(rootIndex + 1),//前序遍历的右子树
        ml = middle.slice(0, rootIndex),//中序遍历的左子树
        mr = middle.slice(rootIndex + 1);//中序遍历的右子树
    root.left = recover(fl, ml);//根据左子树的前序和中序还原左子树并赋值给root.left
    root.right = recover(fr, mr);//根据右子树的前序和中序还原右子树并赋值给root.right
    return root;
}
var root = recover(front, middle);
console.log(root.left);
console.log(root.right);

后序中序还原二叉树代码

var middle = ['f', 'c', 'g', 'a', 'd', 'b', 'e'];
var behind = ['f', 'g', 'c', 'd', 'e', 'b', 'a'];

function recover(middle, behind){
    if(behind == null || middle == null || behind.length ==0 || middle.length == 0 || behind.length !== middle.length) return null;
    let root = new Node(behind[behind.length - 1]);
    let rootIndex = middle.indexOf(root.value);
    let ml = middle.slice(0, rootIndex),
        mr = middle.slice(rootIndex + 1),
        bl = behind.slice(0, rootIndex),
        br = behind.slice(rootIndex, behind.length - 1);
    root.left = recover(ml, bl);
    root.right = recover(mr, br);
    return root;
}
var root = recover(middle, behind);
console.log(root.left);
console.log(root.right);
二叉树的搜索

树的搜索,图的搜索,爬虫的逻辑,搜索引擎的爬虫算法。

深度优先搜索:更适合探索未知
广度优先搜索:更适合探索局域

//深度优先搜索
//对于二叉树来说,深度优先搜索和前序遍历的顺序一样
function deepSearch(root, target){
    if(root == null) return false;
    if(root.value == target) return true;
    let left = deepSearch(root.left, target),
        right = deepSearch(root.right, target));
    return left || right;
}

//广度优先搜索
function wideSearch(rootList, target){
    if(rootList == null || rootList.length == 0) return false;
    let children =[];//当前层所有节点的子节点,都在这个List中,这样传入下一层级的时候,就可以遍历整个层级的节点。
    for(let i = 0; i < rootList.length; i++){
        if(rootList[i] != null && rootList[i].value == target) return true;
        else{
            children.push(rootList[i].left);
            children.push(rootList[i].right);
        }
    }
    return wideSearch(children, target);
}
console.log(wideSearch([a],'f'));
二叉树的比较

遇到二叉树比较时,必须确定该树的 左右子树互换位置后是否仍为同一棵树
如果笔试,无特殊说明,互换后为不同的树;
如果面试,尽量问一下确认。

//原树
var a1 =new Node('a'),
    b1 =new Node('b'),
    c1 =new Node('c'),
    d1 =new Node('d'),
    e1 =new Node('e'),
    f1 =new Node('f'),
    g1 =new Node('g');
a1.left = c1;
a1.right = b1;
c1.left = f1;
c1.right = g1;
b1.left = d1;
b1.right = e1;

//待比较树
var a2 =new Node('a'),
    b2 =new Node('b'),
    c2 =new Node('c'),
    d2 =new Node('d'),
    e2 =new Node('e'),
    f2 =new Node('f'),
    g2 =new Node('g');
a2.left = c2;
a2.right = b2;
c2.left = f2;
c2.right = g2;
b2.left = d2;
b2.right = e2;

function compareTree(root1, root2){
    if(root1 == root2) return true;//同一颗树
    if(root1 == null && root2 != null || root1 != null && root2 == null) return false;//其中 一个为空,一个不为空
    if(root1.value  != root2.value) return false;//相同位置的值不等
    return compareTree(root1.left, root2.left) && compareTree(root1.right, root2.right)
         || compareTree(root1.left, root2.right) && compareTree(root1.right, root.left);
}
二叉树的diff算法
//新增什么、修改什么、删除什么
//{type: "新增", origin: null, now: c2}
//{type: "修改", origin: c1, now: c2}
//{type: "删除", origin: c2, now: null}

function diffTree(root1, root2, diffList){
    if(root1 == root2) return diffList;
    if(root1 == null && root2 != null) {//新增了节点
        diffList.push({type: "新增", origin: null, now: root2});
    }else if(root1 != null && root2 == null){//删除了节点
        diffList.push({type: "删除", origin: root1, now: null});
    }else if(root1.value != root2.value){//修改了节点,相同位置节点值不同
        diffList.push({type: "修改", origin: root1, now: root2});
        diffTree(root1.left, root2.left, diffList);//修改后还要继续进行diff
        diffTree(root1.right, root2.right, diffList);
    }else{
        diffTree(root1.left, root2.left, diffList);
        diffTree(root1.right, root2.right, diffList);
    }
}
var diffList = [];
diffTree(a1, a2, diffList);
console.log(diffList);
图的最小生成树

图的表示法:点集合和边集合

   [a, b, c, d, e]
[  //m表示max
    0, 4, 7, m, m,
    4, 0, 8, 6, m,
    7, 8, 0, 5, m,
    m, 6, 5, 0, 7,
    m, m, m, 7, 0,
]
  1. 普利姆算法(加点法)
    1. 任选一个点作为起点
    2. 找到以当前选中点为起点的路径最短的边
    3. 如果这个边的另一端没有被连通起来,那么就连接
    4. 如果这个边的另一端早已连接,则看倒数第二短的边
    5. 重复2-4直到将所有的点都连通为止。
function Node(value){
    this.value = value;
    this.neighbor = [];
}

var max = Infinity;
var pointSet = [new Node('A'),  new Node('B'), new Node('C'), new Node('D'), new Node('E')];
var distance =[
    [0, 4, 7, max, max],
    [4, 0, 8, 6, max],
    [7, 8, 0, 5, max],
    [max, 6, 5, 0, 7],
    [max, max, max, 7, 0],
];

/**
 * 此方法根据当前已有的节点来进行判断,获取到距离最短的点。
 * 需要传入点集合,边集合,当前已经连接进入的集合
 */
function getMinDisNode(allPoints, distance, nowPointSet){
    let fromNode = null,//线段的起点
        minDisNode = null;//线段的终点
    let minDis = Infinity;
    //根据当前已有的这些点为起点,依次判断连接其他点的距离是多少
    for(let i = 0; i < nowPointSet.length; i++){
        let nowPointIndex =pointSet.indexOf(nowPointSet[i]);
        for(let j = 0; j < distance[nowPointIndex].length; j++){
            let curCompNode = pointSet[j];//当前待比较的节点
            if(nowPointSet.indexOf(curCompNode) < 0  //首先这个点不能为已经接入的点
            && distance[nowPointIndex][j] < minDis){  //其次点之间的距离得为目前的最短距离
                fromNode = nowPointSet[i];
                minDisNode = curCompNode;
                minDis = distance[nowPointIndex][j];
            }
        }
    }
    fromNode.neighbor.push(minDisNode);
    minDisNode.neighbor.push(fromNode);
    return minDisNode;
}

function prim(pointSet, distance, start){
    let nowPointSet = [];
    nowPointSet.push(start);
    //获取最小代价的边
    while(true){
        let minDisNode = getMinDisNode(pointSet, distance, nowPointSet);
        nowPointSet.push(minDisNode);
        if(nowPointSet.length == pointSet.length) break;
    }
}
prim(pointSet, distance, pointSet[2]);
console.log(pointSet);
  1. 克鲁斯卡尔算法(加边法)
    1. 选择最短的边进行连接
    2. 要保证边连接的两端至少有一个点是新的点
    3. 或者这个边是将两个部落进行连接
    4. 重复1-3直到将所有的点都连接到一起。
function Node(value){
    this.value = value;
    this.neighbor = [];
}

var max = Infinity;
var pointSet = [new Node('A'),  new Node('B'), new Node('C'), new Node('D'), new Node('E')];
var distance =[
    [0, 4, 7, max, max],
    [4, 0, 8, 6, max],
    [7, 8, 0, 5, max],
    [max, 6, 5, 0, 7],
    [max, max, max, 7, 0],
];

function canLink(resultList, tempBegin, tempEnd){
    let beginIn = null,
        endIn = null;
    for(let i = 0; i < resultList.length; i++){
        if(resultList[i].indexOf(tempBegin) > -1){
            beginIn = resultList[i];
        }
        if(resultList[i].indexOf(tempEnd) > -1){
            endIn = resultList[i];
        }
    }
    //两个点都是新的点,(都不在任何部落)——可以连接,产生新的部落
    //begin(end)在A部落,end(begin)没有在部落——A部落扩张一个村庄
    //begin在A部落,end在B部落——将AB两个部落合并
    //begin和end在同一个部落——不可以连接
    if(beginIn != null && endIn != null && beginIn == endIn){
        return false;
    }
    return true;
}

function link(resultList, tempBegin, tempEnd){
    let beginIn = null,
        endIn = null;
    for(let i = 0; i < resultList.length; i++){
        if(resultList[i].indexOf(tempBegin) > -1){
            beginIn = resultList[i];
        }
        if(resultList[i].indexOf(tempEnd) > -1){
            endIn = resultList[i];
        }
    }

    if(beginIn == null && endIn == null){//两个点都是新的点,(都不在任何部落)——可以连接,产生新的部落
        resultList.push([tempBegin, tempEnd]);
    }else if(beginIn != null && endIn == null){//begin在A部落,end没有在部落——A部落扩张一个村庄
        beginIn.push(tempEnd);
    }else if(beginIn == null && endIn != null){//end在A部落,begin没有在部落——A部落扩张一个村庄
        endIn.push(tempBegin);
    }else if(beginIn != null && endIn != null && beginIn != endIn){//begin在A部落,end在B部落——将AB两个部落合并
        resultList[resultList.indexOf(beginIn)] = beginIn.concat(endIn);
        resultList.splice(resultList.indexOf(endIn),1);
    }
    tempBegin.neighbor.push(tempEnd);
    tempEnd.neighbor.push(tempBegin);
}

function kruskal(pointSet, distance){
    let minDis = Infinity,
        begin = null,
        end = null;
    let resultList = []; // 二维数组,代表有多少个“部落”
    while(true){
        minDis = Infinity;
        for(let i = 0; i < distance.length; i++){
            for(let j = 0; j < distance[i].length; j++){
                let tempBegin = pointSet[i],
                    tempEnd = pointSet[j];
                if(i != j //去掉自己本身的距离,为0
                && distance[i][j] < minDis
                && canLink(resultList, tempBegin, tempEnd)){
                    minDis = distance[i][j];
                    begin = tempBegin;
                    end = tempEnd;
                }
            }
        }
    link(resultList, begin, end);
    if(resultList.length == 1  //只存在一个“部落”
    && resultList[0].length == pointSet.length)break;
    }
}

kruskal(pointSet, distance);
console.log(pointSet);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值