[攻克算法与数据结构]之线性结构(数组、栈、队列)

现在大环境不太好,找工作也不好找。好一点的工作都对算法有要求,想着多刷几道题提升下自己的逻辑思维能力,于是直接上leetcode了,结果连题目也看不懂,彻底懵逼......痛下决心要补补算法与数据结构相关的知识,扫一扫盲区。

线性结构(Linear List)

  • 线性结构是由n (n>=0)个数据元素(结点)a[0],a[1],a[2]...,a[n-1]组成的有限序列

  • 其中:

    • 数据元素的个数n定位为表的长度 = "list".length() ("list".length()=0 (表里没有一个元素)时称为空表)
    • 将非空的线性表(n>=1)记作:(a[0], a[1], a[2], ..., a[n-1])
  • 常见的线性结构:数据结构、栈结构、队列结构、链表结构

数组结构(Array)

  • 数组(Array)结构是一种重要的数据结构:

    • 几乎是每种编程语言都会提供的一种原生数据结构(语言自带的)
    • 并且我们借助于数组结构来实现其他的数据结构,比如栈(Stack)、队列(Queue)、堆(Heap)
  • 通常数组的内存是连续的,所以数组在知道下标值的情况下,访问效率是非常高的

栈结构(Stack)

基本认识

  • 栈是一种非常常见的数据结构

  • 数组

    • 我们知道数组是一种线性结构,并且可以在数组的任意位置插入和删除数据
    • 但是有时候,我们为了实现某些功能,必须对这种任意性加以 限制。
    • 栈和队列是比较常见的受限的线性结构
  • 栈结构示意图

特点

  • 栈,是一种受限的线性结构,后进先出(LIFO)
  • 其限制是仅允许在表的一端进行插入和删除元素。这一端被成为栈顶,相对地,把另一端成为栈底
  • LIFO(last in first out)表示后进入的元素,第一个弹出栈空间
  • 向一个栈插入新元素又称做进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素
  • 从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素

常见操作

  • push(element):添加一个新元素到栈顶位置
  • pop():移除栈顶的元素,同时返回被移除的元素
  • peek():返回栈顶的元素,不对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它)
  • isEmpty():如果栈里没有任何元素就返回true,否则返回false
  • size():返回栈里的元素个数。这个方法和数组的length属性很类似

实现

实现方式

  • 基于数组实现
  • 基于链表实现

基于数组实现代码

TS常规实现
ts 复制代码
class Stack<T>{
    private data: T[] = [];

    push(element: T): void {
        this.data.push(element)
    }

    pop(): T | undefined {
        return this.data.pop();
    }

    peek(): T | undefined {
        return this.data[this.data.length - 1];
    }

    isEmpty(): boolean {
        return this.data.length === 0;
    }

    size(): number {
        return this.data.length;
    }

    clear(): void {
        this.data = [];
    }
}
TS接口实现

IStack.ts

ts 复制代码
export interface IStack<T> {
    push(element: T): void;
    pop(): T | undefined;
    peek(): T | undefined;
    isEmpty(): boolean;
    size(): number;
    clear(): void
}

ArrayStack.ts

ts 复制代码
import {IStack} from './IStack';
class Stack<T> implements IStack<T>{
    private data: T[] = [];

    push(element: T): void {
        this.data.push(element)
    }

    pop(): T | undefined {
        return this.data.pop();
    }

    peek(): T | undefined {
        return this.data[this.data.length - 1];
    }

    isEmpty(): boolean {
        return this.data.length === 0;
    }

    size(): number {
        return this.data.length;
    }

    clear(): void {
        this.data = [];
    }
}

经典题目

十进制转二进制
  • 如何实现将十进制转二进制

    • 要把十进制转成二进制,可以将该十进制数字和2整除(二进制是满二进一),直到结果是0为止
    • 举个例子,把十进制数字10转化成二进制的数字,过程如下:
  • 具体实现
ts 复制代码
import { Stack } from "./Stack";
function decimalToBinary(decimal: number): string {
    const stack = new Stack();
    let result;
    let binary = '';

    while (decimal > 0) {
        result = decimal % 2
        stack.push(result);
        decimal = Math.floor(decimal / 2);
    }

    while (!stack.isEmpty()) {
        binary += stack.pop();
    }
    return binary
}
有效括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入: s = "()" 输出: true

ts 复制代码
function isValid(s: string): boolean {
    const stack:string[] = [];
    for(let i = 0;i < s.length; i++){
        let c = s[i];
        switch(c){
            case '(':
                stack.push(')');
                break;
            case '[':
                stack.push(']');
                break;
            case '{':
                stack.push('}');
                break;
            default:
                if(c!==stack.pop()){
                    return false;
                }
                break;
        }
    }
    return !stack.length;
};

补充点

在解决问题的时候,要注意 for循环和while循环的使用:

  • 已知循环次数的情况下使用for循环
  • 已知循环结果,不知道循环次数的情况下,使用while循环

队列(queue)

基本认识

队列是一种受限的线性表,先进先出(FIFO First In First Out)

  • 受限之处在于它只允许在队列的前端(front)进行删除操作
  • 而在队列的后端(rear)进行插入操作

队列的使用场景

  • 生活中类似的队列结构

    • 比如电影院,商场,甚至是厕所排队
    • 优先排队的人,优先处理(买票,结账,WC)
  • 开发中队列的使用

    • 打印队列

      • 有五份文档需要打印,这些文档会按照次序放入到打印队列中
      • 打印机会依次从队列中取出文档,优先放入的文档,优先被取出,并且对该文档进行打印
      • 以此类推,直到队列中不再有新的文档
    • 线程队列

      • 在开发中,为了让任务可以并行处理,通常会开启多个线程
      • 但是,我们不能让大量的线程同时运行处理任务。 (占用过多的资源)
      • 这个时候,如果有需要开启线程处理任务的情况,我们就会使用线程队列
      • 线程队列会依照次序来启动线程,并且处理对应的任务
    • 队列还有很多其他应用,后续的很多算法中也会用到队列(比如二叉树的层序遍历常见操作

常见操作

  • enqueue(element):向队列尾部添加一个(或多个)新的项
  • dequeue():移除队列的第一(即排在队列最前面的)项,并返回被移除的元素
  • font/peek():返回队列中第一个元素---最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息)
  • isEmpty():如果队列中不包含任何元素,返回true,否则返回false
  • size():返回队列包含的元素个数,与数组的length属性类似

实现

实现方式

  • 基于数组实现
  • 基于链表实现

基于数组实现代码

IQueue.ts

ts 复制代码
interface IQueue<T> {
    // 入队方法
    enqueue(element: T): void
    // 出队方法
    dequeue(): T | undefined
    // peek
    peek(): T | undefined
    // 判断是否为空
    isEmpty(): boolean
    // 元素个数
    size():number
}

export default IQueue

Queue.ts

ts 复制代码
import IQueue from "./IQueue";
class Queue<T> implements IQueue<T> {
  private data: T[] = [];

  enqueue(element: T): void {
    this.data.push(element);
  }

  dequeue(): T | undefined {
    return this.data.shift();
  }

  peek(): T | undefined {
    return this.data[0];
  }

  isEmpty(): boolean {
    return this.data.length === 0;
  }

  size(): number {
    return this.data.length;
  }
}

注意:与ES5一样,在Class内部可以使用 get 和 set 关键字对某个属性设置存值和取值函数,拦截该属性的存取行为

改进:

ts 复制代码
import IQueue from "./IQueue";
class Queue<T> implements IQueue<T> {
  private data: T[] = [];

  enqueue(element: T): void {
    this.data.push(element);
  }

  dequeue(): T | undefined {
    return this.data.shift();
  }

  peek(): T | undefined {
    return this.data[0];
  }

  isEmpty(): boolean {
    return this.data.length === 0;
  }

  get size(): number {
    return this.data.length;
  }
}

使用:

ts 复制代码
const stack = new Stack<string>();
stack.push('1');
console.log(stack.size); // 1

Stack 和 Queue共同点抽象

Stack 和 Queue 有几个相同的方法,我们可以把共同的方法抽出来进行封装,并通过继承实现 IStack 和 IQueue:

IList.ts

ts 复制代码
interface IList<T> {
  // peek
  peek(): T | undefined;
  // 判断是否为空
  isEmpty(): boolean;
  // 元素个数
  // get size(): number
  size(): number;
}

IStack.ts

ts 复制代码
interface IStack<T> extends IList<T>{
    push(element: T): void
    pop(): T | undefined
}

export default IStack;

IQueue.ts

ts 复制代码
interface IQueue<T> extends IList<T> {
    // 入队方法
    enqueue(element: T): void
    // 出队方法
    dequeue(): T | undefined
}

export default IQueue

经典题目

击鼓传花

  • 击鼓传花是一个常见的面试算法题: 使用队列可以非常方便的实现最终的结果

  • 原游戏规则:

    • 班级中玩一个游戏,所有学生围成一圈,从某位同学手里开始向旁边的同学传一束花
    • 这个时候某个人(比如班长),在击鼓,鼓声停下的一颗,花落在谁手里,谁就出来表演节目
  • 修改游戏规则:

    • 几个朋友一起玩一个游戏,围成一圈,开始数数,数到某个数字的人自动淘汰
    • 最后剩下的这个人会获得胜利,请问最后剩下的是原来在哪一个位置上的人
  • 实现方式:

    • 封装一个基于队列的函数:
    • 参数:所有参与人的姓名,基于的数字
    • 结果:最终剩下的一个人的姓名
  • 实现代码(以数到3的人出队为例):

ts 复制代码
function hotPotato(names: string[], num: number): string {
  // 创建队列结构
  const queue = new ArrayQueue<string>();

  // 将所有的 name 入队
  for (const name of names) {
    queue.enqueue(name);
  }

  while (queue.size() > 1) {
    // 淘汰的规则
    // 数1/2的人先出队,再入队
    for (let i = 1; i < num; i++) {
      // i表示数的数字
      const name = queue.dequeue();
      if (name) {
        queue.enqueue(name);
      }
    }
    // 数3的人淘汰
    queue.dequeue();
  }
  return queue.dequeue();
}

const leftName = hotPotato(["why", "james", "kobe", "curry"], 3);
console.log(leftName);

约瑟夫环问题

  • 阿桥问题(有时也称为约瑟夫斯置换),是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环

    • 人们站在一个等待被处决的圈子里
    • 计数从圆圈中的指定点开始,并沿指定方向围绕圆圈进行
    • 在跳过指定数量的人之后,处刑下一个人
    • 对剩下的人重复该过程,从下一个人开始,朝同一方向跳过相同数量的人,直到只剩下一个人,并被释放
    • 在给定数量的情况下,站在第几个位置可以避免被处决
  • 这个问题是以弗拉维奥·约瑟夫命名的,他是1世纪的一名犹太历史学家

    • 他在自己的日记中写道,他和他的40个战友被罗马军队包围在洞中
    • 他们讨论是自杀还是被俘,最终决定自杀,并以抽签的方式决定谁杀掉谁
  • 击鼓传花和约瑟夫环其实是同一类问题,这种问题还会有其他解法。同样的题目在Leetcode上也有 LCR187

    • 0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字
  • 实现:

ts 复制代码
function iceBreakingGame(num: number, target: number): number {
  // 创建队列结构
  const queue = new ArrayQueue<number>();
  // 所有元素入队
  for (let i = 0; i < num; i++) {
    queue.enqueue(i);
  }
  // 淘汰规则
  while (queue.size() > 1) {
    for (let j = 1; j < target; j++) {
      queue.enqueue(queue.dequeue()!);
    }
    queue.dequeue();
  }
  return queue.dequeue()!;
}

console.log(iceBreakingGame(5, 3));  // 3
console.log(iceBreakingGame(10, 17));// 2

版权所有,如有转载,请注明出处

相关推荐
业精于勤的牙36 分钟前
三角形最小路径和(二)
算法
风筝在晴天搁浅38 分钟前
hot100 239.滑动窗口最大值
数据结构·算法·leetcode
夏乌_Wx1 小时前
练题100天——DAY31:相对名次+数组拆分+重塑矩阵
数据结构·算法
LYFlied1 小时前
【算法解题模板】-解二叉树相关算法题的技巧
前端·数据结构·算法·leetcode
Ven%1 小时前
【AI大模型算法工程师面试题解析与技术思考】
人工智能·python·算法
天勤量化大唯粉1 小时前
枢轴点反转策略在铜期货中的量化应用指南(附天勤量化代码)
ide·python·算法·机器学习·github·开源软件·程序员创富
爱学习的小仙女!2 小时前
算法效率的度量 时间复杂度 空间复杂度
数据结构·算法
AndrewHZ2 小时前
【复杂网络分析】什么是图神经网络?
人工智能·深度学习·神经网络·算法·图神经网络·复杂网络
Swizard2 小时前
拒绝“狗熊掰棒子”!用 EWC (Elastic Weight Consolidation) 彻底终结 AI 的灾难性遗忘
python·算法·ai·训练
fab 在逃TDPIE3 小时前
Sentaurus TCAD 仿真教程(十)
算法