前端算法收录

143 阅读5分钟

说明

本人从事的岗位是前端开发,考虑到自己未来的发展,因此有意识的去学习各方面的知识,以此来提升自己的技术水平。而本文记录的是本人学习算法题的笔记,也算是时刻督导自己学习的日志。因为属于刚学习阶段,因此算法的难度会很低,题量也会很少,而且看视频学习确实会比较费时间,本人尽量做到学到的学会的就记录上。

本文会持续更新,如有希望一起学习进步的读者可自行收藏,同时如有发现文中描述错误的地方,望大佬不吝指出,在此拜谢!

1.将一个数组旋转K步

案例:

  1. 输入一个数组,如[1,2,3,4,5,6,7];
  2. k等于3时,则旋转3步;
  3. 输出[5,6,7,1,2,3,4];

提醒:读者可先分析该案例的题意,并自行设计解题思路,并进行作答。

分析:

  1. 题意很明确,从尾部截取元素至头部,K就是截取的次数,或者说个数;
  2. 为了减少部分边界情况,设计函数建议用ts做类型检测限制;
  3. 数组为空,或者k不存在应返回原值;
  4. k如果为数组长度的倍数,旋转k步之后和原来一样,因此实际步数为k对数组长度取余;
  5. 一种是一个一个尾部移头部,一种是一段一段尾部移头部;

代码:

方法1:pop() + unshift()

/**
 * 使用尾部pop移除再追加头部unshift追加方式
 * @param arr 数组
 * @param k 步数
 * @returns
 */
rotate1(arr: number[], k: number): number[] {
    // 获取数组长度
    const length = arr.length;
    // 空数组或旋转0步直接返回
    if (!length || !k) return arr;
    // 计算实际旋转步数
    const step = Math.abs(k % length);
    // 遍历数组,尾部删一个,头部加一个
    for (let i = 0; i < step; i++) {
        const n = arr.pop();
        if (n) {
            arr.unshift(n);
        }
    }
    return arr;
}

方法2:

rotate2(arr: number[], k: number): number[] {
    // 获取数组长度
    const length = arr.length;
    // 空数组或旋转0步直接返回
    if (!length || !k) return arr;
    // 计算实际旋转步数
    const step = Math.abs(k % length);
    // 截取尾部需要移的元素
    let part1 = arr.slice(-step);
    // 截取移去尾部后剩余的元素
    let part2 = arr.slice(0, length - step);
    // 两部分拼接得到新的旋转后的数组
    let part3 = part1.concat(part2);
    return part3;
}

代码测试:slice() + concat()

// // 性能测试
const arr1 = []
for (let i = 0; i < 10 * 10000; i++) {
    arr1.push(i)
}
// 方法1测试:
console.time('rotate1')
rotate1(arr1, 9 * 10000)
console.timeEnd('rotate1') // 2095.52685546875 ms O(n^2)

// 方法2测试:
const arr2 = []
for (let i = 0; i < 10 * 10000; i++) {
    arr2.push(i)
}
console.time('rotate2')
rotate2(arr2, 9 * 10000)
console.timeEnd('rotate2') // 1.400146484375 ms O(1)

结论:方式2性能优于方法1

原因:

  • 方法1中unshift方法在数组头部插入元素,数组是有序的,头部插入导致后面元素都要重排,耗费性能,同时遍历k步数也要耗费性能,因此时间复杂度O(n^2);
  • 方法2中slice方法是截取后面元素,不改变原有数组排序,concat方法合并两数组,也不会导致数组重排,时间复杂度O(n);

2.两个栈实现一个队列

案例:

  1. 实现两个栈,并用他们实现一个队列的功能;
  2. 队列的功能有add length delete;

分析:

  1. 队列是先进先出的,而栈是先进后出的,这两者不一样;
  2. 要求提到了使用两个栈,我们可以用两个数组实现栈的需求;
  3. 我们让其中一个数组为基准栈,另一个作为中转栈;
  4. 队列add时,把数据压入基准栈中;
  5. 队列delete时,由于需要删除栈底先进的元素,显然直接对基准栈删除元素 是做不到的,此时我们把基准栈的元素都出栈,并压入中转栈,这样基准栈先进的元素会作为中转栈后进的元素,此时我们对中转栈进行一次出栈,即删除了顶部的元素,也就是原基准栈最底部的元素,这样我们就实现了队列的删除,同时不违背栈的原则。当然,最后我们要把中转栈的剩余元素出栈并压入基准栈,因为我们队列是基于基准栈的;
  6. length的话就比较简单了,直接返回基准栈的长度即可;

代码:

// 为了方便,假设这个队列的都是数字,用ts限制:

// 定义队列类
export class MyQueue {
    // 定义两个私有的数组(用来实现栈):
    private stack1: number[] = [];
    private stack2: number[] = [];

    // 加入队列:
    add(n: number) {
        this.stack1.push(n);
    }
    
    // 移除队列
    delete(): number | null {
        // res作为删除队列元素时返回值
        let res;
        while (this.stack1.length) {
            // 出基准栈,入中转栈
            const n = this.stack1.pop();
            if (n) {
                this.stack2.push(n);
            }
        }
        
        // 删除实际需要出队列的元素
        res = this.stack2.pop();
        while (this.stack2.length) {
            // 剩余出中转栈,入基准栈
            const n = this.stack2.pop();
            if (n) {
                this.stack1.push(n);
            }
        }
        // 返回值
        return res || null;
    }
    // 获取长度
    get length() {
        return this.stack1.length;
    }
}

测试:

const myQueue = new MyQueue();
myQueue.add(100);
myQueue.add(200);
myQueue.add(300);
myQueue.add(400);
console.info(myQueue.length); // 4
myQueue.delete();
console.info(myQueue.length); // 3

3.根据数组创建单向链表

案例:根据数组创建一个单向链表

提醒:在此之前,需要对链表有个简单的认知,什么是链表?此处不作介绍,请自行查阅。

分析:

  1. 数组每一个元素都会被改造成对象,对象属性value值为当前元素值,属性next为指向下一个节点对象;
  2. 最后一个元素对象无next属性;
  3. 返回值为第一个节点对象;

代码:

// 定义链表结构接口:
interface ILinkListNode {
    value: number;
    next?: ILinkListNode;
}

/**
 *根据数组创建单向链表
 * @param arr number arr
 * @return 注意,由于是链表,我们只需要返回头部那一个就行,因为前一个next指向下一个
 */
function createLinkList(arr: number[]): ILinkListNode {
    // 获取数组长度
    const length = arr.length;
    // 为空抛出错误
    if (length === 0) throw new Error("arr is empty");
    
    // 由于我们只需要拿到第一个节点,因此从后往前遍历
    // 先获取最后一个(最后一个没有next)
    let curNode: ILinkListNode = {
        value: arr[arr.length - 1],
    };
    // 从后往前遍历,使得倒数第二个next是最后一个,依次类推
    for (let i = length - 2; i >= 0; i--) {
        curNode = {
            value: arr[i],
            next: curNode,
        };
    }
    return curNode;
}

测试:

const arr = [1, 2, 3, 4, 5];
const curNode = createLinkList(arr);
console.info(curNode); 

打印结果如下:

image.png

4.反转单向链表

案例:对单项链表进行反转

分析:

  1. 题意很明确,① -> ② -> ③ -> ④ -> ⑤ 反转成 ⑤ -> ④ -> ③ -> ② -> ①;
  2. 引入变量a,表示第一个,反转的话第一个变最后一个,则需删除next,如此就会失去第二个的引用,因此需要继续引入变量b,表示第二个,此时移除a的next,把b的next指向a,此时第三个的引用就会丢失,所以必须引入第三个变量c,为了方便说明,下面就以preNode、curNode、nextNode来表示三个变量;
  3. 初始化nextNode表示第一个,preNode和curNode为undefined,由于只有nextNode有值,无法进行切换,因此往右移一位,preNode=undefined、curNode=①、nextNode=②,此时删除①即curNode的next,此时① ② -> ③ -> ④ -> ⑤;
  4. 本来应该②的next指向①,即nextNode的next指向curNode,但是此时如果设置,③的数据会丢失,因此需要继续往右移一位,保证③被变量指向着,此时preNode=①、curNode=②、nextNode=③,且① ② -> ③ -> ④ -> ⑤,此时把把②即curNode的next指向①,即curNode的next=preNode,此时① <- ② ③ -> ④ -> ⑤;
  5. 重复4步骤,继续往右移,需要注意,我们右移nextNode是利用next属性移动的,原最后一个节点无next,因此没curNode获取到最后一个节点身上没有next属性,需要手动设置;

提示:为了方便测试,我们此处使用的原链表数据取至【3】中数组生成的链表;

代码:

// 定义链表结构接口:
interface ILinkListNode {
    value: number;
    next?: ILinkListNode;
}

/**
 * 反转单向链表
 * @param listNode 需要反正的链表
 */
function reverseLinkList(listNode: ILinkListNode): ILinkListNode {
    // 如果链表只有一个元素直接返回
    if (!listNode.next) return listNode;
    // 设置三个变量节点
    let preNode: ILinkListNode | undefined = undefined;
    let curNode: ILinkListNode | undefined = undefined;
    let nextNode: ILinkListNode | undefined = listNode;
    
    // 循环右移,只要能获取到下一个节点都右移一次
    while (nextNode) {
        if(curNode && !preNode){
            // 【// @ts-ignore;】注释是用来忽略ts编译在写的过程时报错提示
            // @ts-ignore;
            delete curNode.next; // 删除第一个元素的next
        }
        // 当前节点的next指向上一节点
        if(curNode && preNode){
            // @ts-ignore;
            curNode.next = preNode;
        }
        
        // 右移操作
        preNode = curNode;
        curNode = nextNode;
        nextNode = nextNode?.next; // 最后一个节点没有next,此处用?.可选链防报错
    }
    // 手动处理最后一个节点的next
    curNode!.next = preNode;
    return curNode!;
}

测试:

const arr = [1, 2, 3, 4, 5];
const curNode = createLinkList(arr); // 【3】中数组转链接的函数
console.info(curNode); // 获得原链表

const reverseNode = reverseLinkList(curNode);
console.log(reverseNode);

打印的结果:

image.png

5.链表实现队列

案例:用链表实现队列,有add、delete、length方法

分析:

  1. 考虑到队列先进先出,映射到链表时,实现add加入队列时,必须从尾部加一个next指向,因此需要变量记录尾部数据;
  2. 考虑到出队列是从第一个先出,实现delete得知道第一个数据是什么,因此需要变量记录头部数据;
  3. 考虑到链表的特性,为了拿到队列长度,因此需要一个变量实时记录队列元素个数;

代码:

// 链表接口
interface IListNode {
    value: number | null;
    next: IListNode | null;
}

class MyQueue {
    // 定义三个私有变量
    private head: IListNode | null = null;
    private tail: IListNode | null = null;
    private len = 0;
    // 进队列
    add(n: number) {
        const newNode = {
            value: n,
            next: null,
        };
        //如果头部不存在,则直接赋值给头部
        if (this.head == null) {
            this.head = newNode;
        }

        // 如果尾部存在,则原尾部的next指向新元素
        if (this.tail) {
            this.tail.next = newNode;
        }

        // 更新尾部为新的元素
        this.tail = newNode;

        // 更新队列中元素个数
        this.len++;
    }
    // 出队列
    delete() {
        const headNode = this.head;
        // 不存在返回null
        if (headNode == null) return null;
        if (this.len <= 0) return null;

        // 获取头部的value值作为删除的返回值
        const value = headNode.value;
        // 更新头部元素
        this.head = headNode.next;
        // 更新元素个数
        this.len--;
        return value;
    }
    // 获取长度
    get length(): number {
        return this.len;
    }
}

测试:

const queue = new MyQueue();
queue.add(100);
queue.add(200);
queue.add(300);
queue.add(400);
console.log(queue.length); // 4
queue.delete();
console.log(queue.length); // 3

补充:用链表实现队列的性能跟用数组push和shift实现队列的性能,谁更优?

性能测试:

// 性能测试:
const q1 = new MyQueue();
console.time("queue by list");
for (let i = 0; i < 100000; i++) {
    q1.add(i);
}
for (let i = 0; i < 100000; i++) {
    q1.delete();
}
console.timeEnd("queue by list"); // 13.22607421875 ms

const q2 = [];
console.time("queue by array");
for (let i = 0; i < 100000; i++) {
    q2.push(i);
}
for (let i = 0; i < 100000; i++) {
    q2.shift();
}
console.timeEnd("queue by array"); // 1012.818115234375 ms

结论:很显然使用链表实现队列的方式远远优于使用数组实现队列的方式。

原因:链表是无需的,增删很快,只要指定位置的加next或者去掉即可,而数组的头部删会导致数组重排比较费时间。

6.实现二分查找

补充说明:二分查找的数据必须是有序的

分析: 1.开始和结束索引确定中间索引对应值,和目标值比较,偏小则更新开始索引为当前的,偏大则更新结束索引为当前的,循环操作,直到等于; 2.可以使用循环或者递归实现;

代码:

方式一:循环

/**
 *二分查找:循环方式
 * @param arr 二分查找数组
 * @param target 目标值
 * @return 目标值索引/-1
 */
function binarySearch1(arr: number[], target: number): number {
    const length = arr.length;
    if (length === 0) return -1;

    let startIndex = 0;
    let endIndex = length - 1;

    while (startIndex <= endIndex) {
        // 获取中间位置索引
        const midIndex = Math.floor((startIndex + endIndex) / 2);
        if (target < arr[midIndex]) {
            // 目标值较小,在左侧继续找
            endIndex = midIndex - 1;
        } else if (target > arr[midIndex]) {
            // 目标值较大,在右侧继续找
            startIndex = midIndex + 1;
        } else {
            // 相对则返回
            return midIndex;
        }
    }
    // 没找到返回-1
    return -1;
}

方式二:递归

/**
 * 二分查找:递归方式
 * @param arr 二分查找数组
 * @param target 目标值
 * @param startIndex 开始索引
 * @param endIndex 结束索引
 * @returns 目标值索引/-1
 */
function binarySearch2(arr: number[], target: number, startIndex?: number, endIndex?: number): number {
    const length = arr.length;
    if (length === 0) return -1;

    // 开始和结束不传是手动初始化
    if (!startIndex) startIndex = 0;
    if (!endIndex) endIndex = length - 1;

    // 中间位置
    const midIndex = Math.floor((startIndex + endIndex) / 2);
    const midValue = arr[midIndex];

    // 如果相遇,则结束
    if (startIndex > endIndex) return -1;

    if (target < midValue) {
        // 目标值较小,在左侧继续找
        return binarySearch2(arr, target, startIndex, midIndex - 1);
    } else if (target > midValue) {
        // 目标值较大,在右侧继续找
        return binarySearch2(arr, target, midIndex + 1, endIndex);
    } else {
        // 相对则返回
        return midIndex;
    }
}

测试:

const arr = [1, 7, 13, 45, 47, 98, 234, 456, 561, 899];
const index1 = binarySearch1(arr, 288);
const index2 = binarySearch1(arr, 98);

const index3 = binarySearch2(arr,99);
const index4 = binarySearch2(arr,456);

console.log(index1, index2); // -1 5
console.log(index3, index4); // -1 7