关于一些一维数据结构和部分排序算法解析(大厂面试笔试常客, 如果有细节纰漏欢迎指正)

数据结构和算法(一些比较基础的一维数据结构算法, 如有细节问题和纰漏, 欢迎指正)

数据结构和算法有什么关系

举个栗子, 我们都有搬过家, 搬家的时候,我们如果把东西都零零散散的搬的话, 那么我们可能会效率很慢而且人也很累, 所以我们一定会找些箱子和盒子把零零散散的东西都放进去, 然后去搬这些箱子

数据结构是什么呢?
将上述例子映射到数据结构中, 数据结构对应搬家的时候用的箱子, 我们把可以容纳数据的结构称为数据结构,

算法是什么呢?
算法就是咱们搬家时候搬东西的这个过程

总结

数据结构就是把数据打包起来的容器, 算法就是用来对数据结构进行处理的方法, 数据结构是静态的, 算法是动态的

[一维是线,二维是面, 三维带空间, 四维带时间, 一句顺口溜听听就好哈哈]

线性数据结构(也被称为一维数据结构)

线性数据结构强调的是存储与顺序:代表就是数组和链表

数组

在这里插入图片描述
数组的特性

  1. 存储在物理空间上是连续的
  2. 数组定长(数组的长度不可变):
    可能某些小伙伴说, 我们前端的数组貌似是不定长的, 我们创建一个数组好像不许要给他定长度啊,其实我们感觉我们的数组是不定长的, 但是他的底层一定是定长的,只是js引擎帮我们实现了这个过程, 同时当这个长度达到一定的数值的时候就自动扩容了, 然而这个扩容的过程是消耗性能的, 而一名优秀的工程师应该要学会节约程序的性能
  3. 数组的变量指向了数组第一个元素的位置
const arr = [1, 2, 3];
//这个arr指向的是数组[1, 2, 3]吗? 一定不是, 他指向的是[1, 2, 3]的第一个元素的位置也就是1
arr[1], arr[2], //方括号代表的其实是数组位置的偏移(操作系统知识:通过偏移查询数据性能最好)

数组的优点

  • 查询性能好(唯一的优点)

数组的缺点

  • 因为空间必须是连续的, 所以如果数组比较大,当系统的空间碎片较多的时候, 容易存不下
  • 因为数组的长度是固定的, 所以删除和添加内容都会消耗一定的性能

链表(带有封装性质的数据结构, 同时只要不特殊强调, 我们默认研究的是单向链表)

在这里插入图片描述

注意:链表的每一个节点都认为自己是根节点

直接上代码

//用代码表示上方的链表
//定义创造节点的构造函数
class Node {
    constructor(value) {
        // 每个节点的值
        this.value = value;
        // 每个节点的下一个节点
        this.next = null;
    }
}


const a = new Node(1);
const b = new Node(2);
const c = new Node(3);
const d = new Node(4);
const e = new Node(5);
const f = new Node(6);
a.next = b; // a拿着b的引用
b.next = c; // b拿着c的引用
c.next = d; // c拿着d的引用
d.next = e; // d拿着e的引用
e.next = f; // e拿着f的引用

链表的特性

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

链表的优点

  • 只要内存足够大, 就不用担心空间碎片的问题, 都可以放得下
  • 链表的添加和删除非常的容易

链表的缺点

  • 查询速度慢
  • 链表每一个节点都需要创建一个指向next的引用, 浪费一些空间

来谈谈栈和队列

栈结构就好比是一个箱子, 或者我们可以理解为一把手枪, 在箱子中, 我们放进去一些衣物, 最先放进去的衣服在最底部, 而最后放进去的衣服在最上面, 我们如果需要取第一件放进去的衣服, 则需要先取出压在上面的衣服, 手枪如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EXxbopNW-1574593863190)(./imgs/shouqiang.png)]

由此,栈结构的核心概念即为先入后出

队列

而队列结构相当于一根水管, 我先进去的总是先出来

用js实现栈和队列

//栈
class Stack {
    construtor() {
        this.arr = [];
        this.push = value => {
            arr.push(value);
        }
        this.pop = () => {
            arr.pop();
        }
    }
}
const stack = new Stack();
stack.push(1); //先进的是1
stack.push(2); 
stack.push(3);

stack.pop(); //先释放的肯定是3
// 因为数组的实例方法带来的便捷, 所以js语言中天生就很好实现栈结构,

//队列
class Queue {
     construtor() {
        this.arr = [];
        this.push = value => {
            arr.push(value);
        }
        this.shift = () => {
            arr.shift();
        }
    }
}

扩展 -> JS的执行机制

JS运行的环境叫做宿主环境。

执行栈: call stack, 一个数据结构, 用于存放各种函数的执行环境, 每一个函数执行之前, 它的相关信息都会被放入执行栈, 函数调用之前, 创建执行环境, 然后加入执行栈, 函数调用之后, 销毁执行环境。

JS引擎永远执行的是执行栈的最顶部,

var global = '全局变量先被创建';
function foo() {
    var local = '函数变量第二被创建';
    function insetFoo() {
        var localInset = '函数嵌套的函数最后被创建'
    }
    insertFoo(); 
}
foo();
// 当js文件被解析时, GO总是最先被创建的, 而AO总是最先被释放的

浏览器宿主环境中包含5个线程:

  1. JS引擎: 负责执行执行栈的最顶部代码
  2. GUI线程: 负责渲染页面
  3. 事件监听线程: 负责监听各种事件
  4. 计时线程: 负责计时
  5. 网络线程: 负责网络通信

当上面的线程发生了某些事情, 如果该线程发现, 他会将处理程序加入到一个叫做事件队列的内存, 当js引擎发现执行栈里已经没有了任何内容以后, 会将事件队列中的第一个函数加入到执行栈中执行。

JS引擎对事件队列的取出执行方式, 以及与宿主环境的配合, 称之为事件循环。

事件队列在不同的宿主环境中有所差异, 大部分宿主环境会将事件队列进行细分, 在浏览器中, 事件队列分为两种:

  1. 宏任务: MacroTask, 计时器结束的回调, 事件回调, http回调等绝大部分异步函数进入宏队列。
  2. 微任务:MutationObserver, Promise产生的回调进入微队列

当执行栈清空时, JS引擎会首先将微任务中的所有任务执行结束, 如果没有微任务则执行宏任务

线性数据结构的一小部分算法(遍历为主)

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

数组的遍历

//遍历数组 -> for循环
function forArray(arr) {
    //严谨性判断
   if(arr instanceOf Array) {
        for(let i = 0, len = arr.length; i < len; i++) {
        console.log(arr[i]);
    }
   }
}
//遍历数组 -> forEach
function forArray(arr) {
    //严谨性判断
    if(arr instanceOf Array) {
        arr.forEach((index, item, el) => {
            console.log(item);
        })
    }
}
// ...还有很多遍历的方法就不一一列举了
const arr = [1, 2, 3];
forArray(arr); // 1, 2, 3

链表的遍历

class CreateNode {
    constructor(value) {
        this.value = value;
        this.next = null;
    }
}

const a = CreateNode(1);
const b = CreateNode(2);
const c = CreateNode(3);
a.next = b;
b.next = c;

//递归遍历链表
function forLink(root) {
    // 严谨性判断和递归判断
    if(!root || !root.next) {
        return;
    }
    console.log(root.value);
    //递归
    forLink(root.next);
}

forLink(a);

链表的逆置

话不多说, 字面意思大家都懂, 链表逆置就是把链表倒过来, 代码需要对递归算法理解比较深, 如果看不懂可以私信给你解释的明明白白(这里就默认你看得懂啦),直接上代码

//链表的逆置
class CreateNode {
    constructor(value) {
        this.value = value;
        this.next = null;
    }
}

const a = new CreateNode(1);
const b = new CreateNode(2);
const c = new CreateNode(3);
a.next = b;
b.next = c;

// 逆置函数
function reverseLink(root) {
    // 严谨性判断
    if(!root) {
        return;
    }
    if(root.next.next === null) { 
    //这判断条件的意思就是说, 如果传进来的链表的根节点的下一个的下一个指向为空
    //比如也就是说我现在要找到是否是倒数第二个节点, 因为我找到最后一个没有意义
    //我找到倒数第二个我让最后一个的next指向倒数第二个
        root.next.next = root; // root.next是最后一个,root是当前这个, 本来最后一个的next为null, 我让他逆置为倒数第二个
        return root.next; // 然后返回最后一个
    }
    // 如果不是倒数第二个, 我要开启递归
   const resp =  reverseLink(root.next); // 一直到找到返回最后一个为止 他一定会找到最后一个有return的值, 而且由于这是一个同步操作,会一直等到这一块代码的响应结果,递归效应不多说
   root.next.next = root;
   root.next = null;
   return resp;
}

// 这个时候我们来测验一下我们的逆置有没有成功
const resp = reverseLink(a);
    function forLink(root) {
        // 严谨性判断 + 递归出口
        if(!root) {
            return;
        }
        console.log(root.value);
        forLink(root.next);
    }

forLink(resp); // 返回6 -> 5 -> 4 -> 3 -> 2 -> 1

一维数据结构的排序算法(通常为数组)

排序算法不分优劣, 只看适应的场景

现在有一个数组, 需求是要我们对他进行排序, 我们先来看看以前我们是怎处理的

let arr = [7,3,6,4,5,0,1,8,2];
// 首先我们应该想到的是数组的实例方法sort
arr.sort((a, b) => {
    return a - b; 
})
console.log(arr); // 一定是排好序的
// 如果我们想要自己写一份排序也行
let arr = [7,3,6,4,5,0,1,8,2];

//有一个getMin函数, 我们传入一个arr, 期待他给我们返回这个数组里最小的值,并且将原数组中这个值删除掉
function getMin(arr) {
    // 初始defaultIndex为0
    let defaultIndex = 0;
    for(let i = 0, len = arr.length; i < len; i++) {
        if(arr[i] < arr[defaultIndex]) {
            defaultIndex = i;
        }
    }
    // 在数组删除这个最小值之前,我们先用一个变量保存起来
    let lastMinValue = arr[defaultIndex];
    arr.splice(defaultIndex, 1);
    return lastMinValue;
}

//有一个sort函数, 我们传入一个arr, 期待他给我们返回一个新的数组, 而这个数组是从小到大排序过的
function sort(arr) {
    let lastArr = []; // 定义一个最后要返回的lastArr;
    for(let i = 0, len = arr.length; i < len; i++) {
        //lastArr每一次的值都是通过getMin函数得来的最小的值
        lastArr[i] = getMin(arr);
    }
}

const newArr = sort(arr);
console.log(newArr); // 返回结果一定是排序过后的值

//其他的方法就不写了... 就是举个例子

其实第二种方式代码相当复杂, 循环也特别多, 而sort方法其实我本人的话突然忘记了他的底层是不是冒泡排序, 不管是不是吧, 冒泡排序确实在各方面都会比第二种方式出色很多,而我的猜测sort大概也是冒泡排序

冒泡排序
在写冒泡排序之前, 我们需要明白一个道理, 排序的本质是什么? 排序的本质是比较和交换

// 我们可以尝试用冒泡排序还原一下sort方法
     let arr = [7,3,6,4,5,0,1,8,2];  
        //mySort接收一个比较函数
        Array.prototype.mySort = function (compareFunc) {
            // 严谨性判断
            if(typeof compareFunc != 'function') {
                return;
            }
            const exchange = (arr, a, b) => {
                // 交换位置函数
                const obj = {a: arr[a], b: arr[b]}
                arr[a] = obj.b;
                arr[b] = obj.a;
            }
            //由于每一轮都会选出一个比较的结果放到最后, 所以我们只需要控制多几次循环其实可以完成整个的冒泡排序
           for(let i = 0,len = this.length; i < len; i ++) {
            for(let i = 0, len = this.length; i < len - 1; i++) {
                if(compareFunc(this[i], this[i+1]) > 0) {
                    // 执行exchange函数
                    exchange(this,i, i + 1);
                }
            }
           }
        }
        arr.mySort((a, b) => {
            return a - b;
        });
        console.log(arr);

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

// 一样的写个mySort, 但是是用选择排序来写的
    let arr = [7,3,6,4,5,0,1,8,2];
    console.log(arr);
    // 在数组原型上定义一个排序方法, 传入一个compare函数, 我们期待他将调用该方法的数组重新按照compare的规则排序以后返回给我们
    Array.prototype.mySort = function(func) {
        // 严谨性判断
        if(typeof func != 'function') {
            return;
        } 
        const exchange = (arr, a, b) => {
            // 交换函数
            let obj = {
                a: arr[a],
                b: arr[b]
            }
            arr[a] = obj.b;
            arr[b] = obj.a;
        }
        // 开始遍历该数组
        let maxIndex = 0; // 同时我们要知道最大值的索引为max
       for(let i = 0, len = this.length; i < len; i++) {
           let temp = i;
        // 这一块有个坑, 要不你就用var 如果用let的话作用域会被锁死
        for(let i = 0, len = this.length; i < len - temp; i++) {
            // 每一轮我们会求出一个最大值的索引
            if(func(arr[maxIndex], this[i]) < 0) {
                maxIndex = i;
            }
        }
        exchange(this, maxIndex, this.length - 1 - i);
       }
    }   
    arr.mySort((a, b) => {
        return a - b;
    })
    console.log(arr); // 肯定是排好序的

简单快速排序
快速排序我们其实可以理解为小学时候的站队, 体育老师说以体育委员作为基准, 比他高的站他左边, 比他矮的站他右边, 然后体育委员左边的再找一个人为基准比他高的站左边矮的站右边,知道排好队列, 快速排序也是常用的一维数据结构的算法,这里给大家介绍快速排序的简单版让大家理解思维, 等会介绍标准版加深理解

let arr = [7,3,6,4,5,0,1,8,2];
//比较规则函数
function compareFunc(a, b) {
return a - b;
}
// 定义一个sort方法, 这里我就不在数组原型上定义了, 看了之前两个例子在原型上定义实例方法应该都不是难事了, sort方法接受一个arr和一个compare函数作为参数, 并将该数组排序过后返回
function sort(arr, compareFunc) {
    if(!arr || arr.length === 0) {
        return [];
    }
    // centerValue就等于是体育委员
    let centerValue = arr[0];
    let left = [];  //比体育委员小的我们就放进左边数组
    let right = [];  //比体育委员大的我们就放进右边数组
    for(let i = 1, len = arr.length; i < len; i++) {
        if(compareFunc(centerValue, arr[i]) > 0) {
            left.push(arr[i]);
        }else {
            right.push(arr[i]);
        }
    }
    // 上面排完了以后 left 和right 各自有了值,这时候体育委员在中间, 但是体育委员左右的值都不是排序好的, 所以我们需要在左右重新递归调用自己给left和right排序,而最后left和right只要一个数值的时候, 他们再调用sort方法实际上left和right是push不进去值的, 这就是递归出口
    left = sort(left, compareFunc);
    right = sort(right, compareFunc);
    left.push(centerValue);   // 要记得把体育委员拼接一下别忘记了
    arr = left.concat(right);  // 连接数组
    return arr;
}
console.log(sort(arr, compareFunc)); // 返回结果一定是排好序的

标准快速排序

console.log('笔者暂时对标准快排理解不是那么深,也是仅仅只会写,写出来的可能跟网上的其他教程也差不多, 还在探索中..后续补上')

排序算法还有许多其他的种类, 篇幅+时间有限, 以后可能会慢慢加入进来, thanks for reading

发布了33 篇原创文章 · 获赞 11 · 访问量 2269

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/103227536