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

现在大环境不太好,找工作也不好找。好一点的工作都对算法有要求,想着多刷几道题提升下自己的逻辑思维能力,于是直接上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

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

相关推荐
Themberfue6 分钟前
基础算法之双指针--Java实现(下)--LeetCode题解:有效三角形的个数-查找总价格为目标值的两个商品-三数之和-四数之和
java·开发语言·学习·算法·leetcode·双指针
陈序缘32 分钟前
LeetCode讲解篇之322. 零钱兑换
算法·leetcode·职场和发展
-$_$-34 分钟前
【LeetCode HOT 100】详细题解之二叉树篇
数据结构·算法·leetcode
大白飞飞36 分钟前
力扣203.移除链表元素
算法·leetcode·链表
学无止境\n1 小时前
[C语言]指针和数组
c语言·数据结构·算法
黄俊懿1 小时前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
新缸中之脑1 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
夜雨翦春韭1 小时前
【代码随想录Day29】贪心算法Part03
java·数据结构·算法·leetcode·贪心算法
Curry_Math2 小时前
Acwing 区间DP & 计数类DP
算法
Tisfy2 小时前
LeetCode 1928.规定时间内到达终点的最小花费:动态规划
算法·leetcode·动态规划·