线性表 —— 数组、栈、队、链表

本文以 typescript 实现数据结构,虽说是 ts 实现,但更准确说是面向对象的方式实现,因此可以无缝切换成 Java 等面向对象语言。

什么是数据结构(Data Structure)?

  • “数据结构是ADT(抽象数据类型 Abstract Data Type)的物理实现。” — 《数据结构与算法分析》
  • “数据结构(data structure)是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以 带来最优效率的算法。” —中文维基百科

线性表 —— 数组、栈、队、链表_第1张图片

线性结构

◼ 线性结构(英語: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])。
 数据元素a[i](0≤i≤n-1)只是个抽象符号,其具体含义在不同情况下可以不同。
◼ 上面是维基百科对于线性结构的定义,有一点点抽象,其实我们只需要记住几个常见的线性结构即可

线性表 —— 数组、栈、队、链表_第2张图片

数组

数组(Array)结构是一种重要的数据结构:
 几乎是每种编程语言都会提供的一种原生数据结构(语言自带的);
 并且我们可以借助于数组结构来实现其他的数据结构,比如栈(Stack)、队列(Queue)、堆(Heap);

概念

◼ 栈也是一种 非常常见 的数据结构, 并且在程序中的 应用非常广泛。
◼ 数组
 我们知道数组是一种线性结构, 并且可以在数组的 任意位置 插入和删除数据。
 但是有时候, 我们为了实现某些功能, 必须对这种任意性 加以 限制。
 而 栈和队列 就是比较常见的 受限的线性结构, 我们先来学习栈结构。

可以看到 栈 就是受限的线性结构,重点就是受限。它就像数组的“子集”一样。
因此不是严肃使用栈的场景,我们完全可以把数组来模拟当栈使用,比如刷题的时候。只是注意要自我限制,只用栈有的那几个方法,别突然用下标访问数组数据,那就已经不是栈了。

线性表 —— 数组、栈、队、链表_第3张图片

数组和链表都能实现。
js 中没有自带的链表结构,但 Java 中有 LinkList,它底层就是链表。ArrayList 是数组。

常见方法

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

实现

/**
 * 栈接口。后续想要实现栈,就 implement 这个接口。
 * @example
 * class ArrayStack implements Stack
 */
export interface Stack<T> {
    push(value: T): void;
    pop(): T | undefined;
    peek(): T | undefined;
    size(): number;
    isEmpty(): boolean;
}

import { Stack } from './Stack';

/*
 * 基于数组的栈
 */
export class ArrayStack<T> implements Stack<T> {
    private data: T[];

    constructor() {
        this.data = [];
    }

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

    pop() {
        return this.data.pop();
    }

    peek() {
        return this.data[this.data.length - 1];
    }

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

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

题目

进制转换

// 十进制转二级制

import { ArrayStack } from '../ArrayStack';

/**
 * 首先 js 自带了进制转换的方法:Number(10).toString(2); 1010
 * @param decimal 10进制
 * @returns
 */
export function decimalToBinary(decimal: number) {
    return decimal.toString(2);
}

// 示例
// console.log(decimalToBinary(10)); // 输出:1010

/**
 * 栈实现
 * @param decimal 10进制
 * @param target 目标进制
 */
export function decimalToBinaryByStack(decimal: number, target: number = 2) {
    const stack = new ArrayStack();
    let remainder: number;

    while (decimal > 0) {
        remainder = decimal % target;
        stack.push(remainder);
        decimal = (decimal - remainder) / target;
    }
    let res = "";
    while (!stack.isEmpty()) {
        res += stack.pop();
    }

    return res;
}

有效的括号

  • https://leetcode.cn/problems/valid-parentheses/
/**
 * 思路:
 * 括号分成左右两部分
 * 遍历字符串只要碰到左部分,就把对应的右括号入栈。这样入栈,栈顶元素一定是最先成对的括号。入栈另一半只是为了更好的比较。
 * 这样只要遍历碰到右部分,就立即与栈顶比较,看是否一致,一致就出栈,表示一对已经配对完毕。
 */

import { ArrayStack } from '../ArrayStack';

export function isValid(s: string) {
    const stack = new ArrayStack<string>();
    const map = {
        '(': () => stack.push(')'),
        '[': () => stack.push(']'),
        '{': () => stack.push('}')
    };
    for (let i = 0; i < s.length; i++) {
        const fn = map[s[i]];
        if (fn) {
            fn();
        } else if (s[i] !== stack.peek()) {
            return false;
        } else {
            stack.pop();
        }
    }

    return stack.isEmpty();
}

概念

队列也是一种受限的线性结构。

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

线性表 —— 数组、栈、队、链表_第4张图片

数组和链表同样都能实现队列,但是链表效率更高。
因为队列涉及到首尾元素的删除。栈删除元素,我们可以让数组最后一个位置为栈顶,这样就不用移动所有元素,所以用数组和链表区别不大。但队列不行,如果用数组一定会移动所有元素。

常见方法

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

实现

数组实现

export interface Queue<T> {
    enqueue(element: T): void;
    dequeue(): T | undefined;
    peek(): T | undefined;
    isEmpty(): boolean;
    // 设为 getter,使用的时候就可以把 size 当属性使用(queue.size),而不是方法了(queue.size())
    get size(): number;
}

import { Queue } from './Queue';

export class ArrayQueue<T> implements Queue<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;
    }
}

队列和栈同为线性结构,它们都有相同的方法,如表长度,获取表元素 peek,表是否为空。
因此可以进一步抽象出接口。

export interface List<T> {
    get size(): number;
    isEmpty(): boolean;
    peek(): T | undefined;
}

import { List } from "../list/List";

export interface Queue<T> extends List<T> {
    enqueue(element: T): void;
    dequeue(): T | undefined;
}

链表实现

题目

击鼓传花 / 烫手山芋(hotPotato)

  • https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof

◼ 原游戏规则:
 班级中玩一个游戏,所有学生围成一圈,从某位同学手里开始向旁边的同学传一束花。
 这个时候某个人(比如班长),在击鼓,鼓声停下的一颗,花落在谁手里,谁就出来表演节目。
◼ 修改游戏规则:
 我们来修改一下这个游戏规则。
 几个朋友一起玩一个游戏,围成一圈,开始数数,数到某个数字的人自动淘汰。
 最后剩下的这个人会获得胜利,请问最后剩下的是原来在哪一个位置上的人?
◼ 封装一个基于队列的函数:
 参数:所有参与人的姓名,基于的数字;
 结果:最终剩下的一人的姓名;

测试数据:["John", "Jack", "Camila", "Ingrid", "Carl"]

这种测试数据要循环使用,而不是遍历一遍就够了的问题,是否就是适合队列来解决呢?

/**
 * 思路:
 * 元素全部进队后,依次出队,并计数,计数不是目标数字的就从队尾重新进队,是目标数字就不再进队
 * 一直重复出队进队,直到队里只剩下一个人,这个人就是幸存者
 */

import { ArrayQueue } from "../ArrayQueue";

export function hotPotato(elements: string[], num: number): number {
    const queue = new ArrayQueue<string>();
    elements.forEach(element => queue.enqueue(element));
    let count = 0;

    while (queue.size > 1) {
        const el = queue.dequeue()!;
        count++;
        if (count === num) {
            count = 0;
        } else {
            queue.enqueue(el);
        }
    }
    const res = queue.dequeue();
    return elements.findIndex(item => item === res);
}

约瑟夫环问题

历史:

◼ 阿桥问题(有时也称为约瑟夫斯置换),是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。
 人们站在一个等待被处决的圈子里。
 计数从圆圈中的指定点开始,并沿指定方向围绕圆圈进行。
 在跳过指定数量的人之后,处刑下一个人。
 对剩下的人重复该过程,从下一个人开始,朝同一方向跳过相同数量的人,直到只剩下一个人,并被释放。
 在给定数量的情况下,站在第几个位置可以避免被处决?
◼ 这个问题是以弗拉维奥·约瑟夫命名的,他是1世纪的一名犹太历史学家。
 他在自己的日记中写道,他和他的40个战友被罗马军队包围在洞中。
 他们讨论是自杀还是被俘,最终决定自杀,并以抽签的方式决定谁杀掉谁。

约瑟夫环问题其实就是击鼓传花问题,它还有很多名字。

  • 剑指offer 62题:圆圈中的最后剩下的数字
  • LeetCode:LCR 187. 破冰游戏

你可能感兴趣的:(数据结构与算法,链表,数据结构,栈,队列,线性表)