现在大环境不太好,找工作也不好找。好一点的工作都对算法有要求,想着多刷几道题提升下自己的逻辑思维能力,于是直接上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:
输入: 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
版权所有,如有转载,请注明出处