解决问题的过程中,不仅仅 数据的存储方式会影响效率,算法的优劣也会影响效率
什么是算法?
算法的通俗理解
线性结构(Linear List)是由n(n≧0)个数据元素(结点)a[0],a[1],a[2],…,a[n-1]组成的有限序列。
其中:
常见线性结构:
数组结构(Array)
栈结构(Stack)
队列结构(Queue)
链表结构(LinkedList)
数组(Array)结构是一种重要的数据结构
TypeScript中数组的各种用法,和JavaScript是一致的:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array
栈 也是一种非常常见的数据结构,并且在程序中的应用非常广泛。
数组:
❓题目:
有六个元素6,5,4,3,2,1顺序入栈,问下列哪一个不是合法的出栈序列?()
A. 5 4 3 6 1 2
B. 4 5 3 2 1 6
C. 3 4 6 5 2 1
D. 2 3 4 1 5 6
分析:
栈的常见操作
import { IStack } from './IStack';
class ArrayStack<T = any> implements IStack<T> {
// 定义一个数组/链表,用于存储元素
private data: T[] = [];
// 实现栈中的操作方法
// push(element):添加一个新元素到栈顶
push(element: T): void {
this.data.push(element);
}
// pop():删除栈顶元素,返回被移除的元素。
pop(): T | undefined {
return this.data.pop();
}
// peek():仅返回栈顶元素,不对栈做任何修改。
peek(): T | undefined {
return this.data[this.data.length - 1];
}
// isEmpty():判断栈里是否有元素。有,返回true,否,返回false。
isEmpty(): boolean {
return this.data.length === 0;
}
// size():返回栈里的元素个数。这个方法和数组的length属性类型。
size(): number {
return this.data.length;
}
}
export default ArrayStack;
export interface IStack<T> {
push(element: T): void;
pop(): T | undefined;
peek(): T | undefined;
isEmpty(): boolean;
size(): number;
}
为什么需要十进制转二进制?
如何实现时十进制转二进制(整数)?
简单实现
import ArrayStack from './02_实现栈结构Stack(重构)';
function decimalToBinary(decimal: number): string {
// 1. 创建一个栈,用于存储余数
const stack = new ArrayStack<number>();
// 2. 使用循环:while:不确定次数,只知道循环结束条件
while (decimal > 0) {
const result = decimal % 2;
stack.push(result);
decimal = Math.floor(decimal / 2);
}
// 3.将所有的余数已放入到stack中,依次取出所有余数
let binary = '';
while (!stack.isEmpty()) {
binary += stack.pop();
}
return binary;
}
export {};
console.log('35:', decimalToBinary(35));
console.log('100:', decimalToBinary(100));
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s
,判断字符串是否有效。
示例 1:
输入:s = “()”
输出:true
示例 2:
输入:s = “()[]{}”
输出:true
示例 3:
输入:s = “(]”
输出:false
⚠️ 提示:
代码
import ArrayStack from './02_实现栈结构Stack(重构)';
function isValid(s: string): boolean {
// 1. 创建一个栈结构:
const stack = new ArrayStack<string>();
// 2. 遍历字符串
for (let i = 0; i < s.length; i++) {
const 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.isEmpty();
}
console.log(isValid('()'));
console.log(isValid('()[]{}'));
console.log(isValid('(]'));
console.log(isValid('([])'));
console.log(isValid('({[}])'));
受限的线性结构
队列(Queue),它是一种受限的线性结构,先进先出(FILO:First In Last Out)
import { IQueue } from './IQueue';
class ArrayQueue<T = any> implements IQueue<T> {
// 内部通过数组保存
private data: T[] = [];
// 队列常见操作
/**
* enqueue(element):向队列尾部添加一个(或者多个)新的项。
* dequeue():移除队列的第一(即排在队列最前面的)项,也是最先移除的元素;
* front/peek():返回队列中第一个元素——最先被添加,也是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息)
* isEmpty():如果队列中不包含任何元素,返回true,否则返回false;
* size():返回队列包含的元素个数,与数组的length属性类似
*/
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;
}
}
export default ArrayQueue;
import type { IList } from '../types/IList';
export interface IQueue<T> extends IList<T> {
enqueue(element: T): void;
dequeue(): T | undefined;
}
export interface IList<T> {
peek(): T | undefined;
isEmpty(): boolean;
get size(): number;
}
击鼓传花,是一个常见的面试算法题:使用队列可以非常方便的实现最终的结果
原游戏规则
修改游戏规则
封装一个基于队列的函数:
分析:
代码
import ArrayQueue from './01_基于数组的队列结构Queue';
function hotPatato(names: string[], num: number) {
if (names.length === 0) return -1;
// 1. 创建一个队列结构
const queue = new ArrayQueue<string>();
// 2. 将所有的name加入队列
for (const name of names) {
queue.enqueue(name);
}
// 3. 淘汰的规则
while (queue.size > 1) {
// 小于 num 的不淘汰
for (let i = 1; i < num; i++) {
const name = queue.dequeue();
if (name) queue.enqueue(name);
}
// num 淘汰
queue.dequeue();
}
// 取出最后一个人
const leftName = queue.dequeue()!;
// 取出最后一个人的原索引
const index = names.indexOf(leftName);
return index;
}
const leftIndex = hotPatato(['why', 'JIM', 'JANE', 'LENO', 'CURRY', 'TIM', 'TOM', 'JERRY', 'JENO', 'TIA'], 5);
console.log(leftIndex);
什么是约瑟夫环问题(历史)?
阿乔问题(有时也称为约瑟夫斯置换),是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。
Leecode 剑指 Offer 62. 圆圈中最后剩下的数字
https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。
求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
示例 1:
输入: n = 5, m = 3
输出: 3
示例 2:
输入: n = 10, m = 17
输出: 2
代码
import ArrayQueue from './01_基于数组的队列结构Queue';
function lastRemaining(n: number, m: number): number {
const queue = new ArrayQueue<number>();
for (let i = 0; i < n; i++) {
queue.enqueue(i);
}
while (queue.size > 1) {
for (let i = 1; i < m; i++) {
queue.enqueue(queue.dequeue()!);
}
queue.dequeue();
}
return queue.dequeue()!;
}
console.log(lastRemaining(5, 3));
console.log(lastRemaining(10, 17));
数组:
但是,数组有很多缺点
要存储多个元素,另外一个选择就是 链表
不同于数组,链表中的元素在内存中 不必是连续的空间。
❣️ 相对于数组,链表的优势有
相对于数组,链表的缺点有
// 1. 创建Node节点类
class Node<T> {
value: T;
next: Node<T> | null = null;
constructor(value: T) {
this.value = value;
}
}
// 2.创建LinkedList类
class LinkedList<T> {
head: Node<T> | null = null;
private size: number = 0;
get length() {
return this.size;
}
}
const linkedList = new LinkedList<string>();
console.log(linkedList.head);
export {};
// 追加节点
append(value: T) {
// 1.根据value创建一个新节点
const newNode = new Node(value);
// 1. 链表本身为空,新添加数据时唯一的节点
// 2. 链表本身不为空,需要向其他节点后面追加节点
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
// current此时,肯定是指向最后一个节点
current.next = newNode;
}
this.size++;
}
insert(position,element):向链表的特定位置插入一个新的项
//插入 insert(position, element): 向链表的特定位置插入一个新的项;
insert(value: T, position: number): boolean {
if (position < 0 || position > this.size) return false;
// 是否添加到第一个
const newNode = new Node(value);
if (position === 0) {
newNode.next = this.head;
this.head = newNode;
} else {
let current = this.head;
let previous: Node<T> | null = null;
let index = 0;
while (index++ < position && current) {
previous = current;
current = current.next;
}
// index === position
newNode.next = current;
previous!.next = newNode;
}
this.size++;
return true;
}
// 获取get
get(position: number): T | null {
if (position < 0 || position >= this.size) return null;
// 查找元素
let index = 0;
let current = this.head;
while (index++ < position && current) {
current = current?.next;
}
// index === position
return current?.value ?? null;
}
// 获取索引
indexOf(value: T): number {
// 遍历
let current = this.head;
let index = 0;
while (current) {
if (current.value === value) {
return index;
}
current = current.next;
index++;
}
return -1;
}
// 更新
update(value: T, position: number): boolean {
if (position < 0 || position >= this.size) return false;
let currentNode = this.head;
let index = 0;
while (index++ < position && current) {
currentNode = currentNode.next;
}
currentNode!.value = value;
return true;
}
// 删除
removeAt(position: number): T | null {
// 越界判断
if (position < 0 || position >= this.size) return null;
// 删除元素
// 判断是否是删除第一个节点
let current = this.head;
if (position === 0) {
this.head = this.head?.next ?? null;
} else {
let previous: Node<T> | null = null;
let index = 0;
while (index++ < position && current) {
previous = current;
current = current.next;
}
// index === position
previous!.next = current?.next ?? null;
}
this.size--;
return current?.value ?? null;
}
// 根据value删除
remove(value: T): T | null {
const index = this.indexOf(value);
return this.removeAt(index);
}
// 链表是否为空
isEmpty(): boolean {
return this.size === 0;
}
// 1. 创建Node节点类
class Node<T> {
value: T;
next: Node<T> | null = null;
constructor(value: T) {
this.value = value;
}
}
// 2.创建LinkedList类
class LinkedList<T> {
private head: Node<T> | null = null;
private size: number = 0;
get length() {
return this.size;
}
// 封装私有方法:
// 根据position 获取到当前节点(不是节点的value,而是获取节点)
private getNode(position: number): Node<T> | null {
let current = this.head;
let index = 0;
while (index++ < position && current) {
current = current.next;
}
return current;
}
// 追加节点
append(value: T) {
// 1.根据value创建一个新节点
const newNode = new Node(value);
// 1. 链表本身为空,新添加数据时唯一的节点
// 2. 链表本身不为空,需要向其他节点后面追加节点
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
// current此时,肯定是指向最后一个节点
current.next = newNode;
}
this.size++;
}
// 遍历链表的方法
traverse() {
const values: T[] = [];
let current = this.head;
while (current) {
values.push(current.value);
current = current.next;
}
console.log(values.join(' -> '));
}
// 插入 insert(position, element): 向链表的特定位置插入一个新的项;
insert(value: T, position: number): boolean {
if (position < 0 || position > this.size) return false;
// 是否添加到第一个
const newNode = new Node(value);
if (position === 0) {
newNode.next = this.head;
this.head = newNode;
} else {
let previous = this.getNode(position - 1);
// index === position
newNode.next = previous?.next ?? null;
previous!.next = newNode;
}
this.size++;
return true;
}
// 根据位置删除
removeAt(position: number): T | null {
// 越界判断
if (position < 0 || position >= this.size) return null;
// 删除元素
// 判断是否是删除第一个节点
let current = this.head;
if (position === 0) {
this.head = this.head?.next ?? null;
} else {
// 重构
let previous = this.getNode(position - 1);
// index === position
previous!.next = previous?.next?.next ?? null;
}
this.size--;
return current?.value ?? null;
}
// 获取get
get(position: number): T | null {
if (position < 0 || position >= this.size) return null;
// 查找元素
return this.getNode(position)?.value ?? null;
}
// 更新
update(value: T, position: number): boolean {
if (position < 0 || position >= this.size) return false;
const currentNode = this.getNode(position);
currentNode!.value = value;
return true;
}
// 获取索引
indexOf(value: T): number {
// 遍历
let current = this.head;
let index = 0;
while (current) {
if (current.value === value) {
return index;
}
current = current.next;
index++;
}
return -1;
}
// 根据value删除
remove(value: T): T | null {
const index = this.indexOf(value);
return this.removeAt(index);
}
// 链表是否为空
isEmpty(): boolean {
return this.size === 0;
}
}
const linkedList = new LinkedList<string>();
linkedList.append('aaa');
linkedList.append('bbb');
linkedList.append('ccc');
linkedList.insert('abc', 0);
linkedList.insert('abcd', 0);
linkedList.insert('中间444', 2);
linkedList.insert('nba', 6);
linkedList.traverse();
linkedList.removeAt(0);
linkedList.traverse();
linkedList.removeAt(2);
linkedList.traverse();
console.log('-----测试get-----');
console.log(linkedList.get(0));
console.log(linkedList.get(1));
console.log(linkedList.get(2));
console.log('-----测试update-----');
console.log(linkedList.update('11111', 1));
linkedList.traverse();
console.log('-----测试indexOf-----');
console.log(linkedList.indexOf('11111'));
linkedList.traverse();
export {};
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList 类:
class Node<T> {
val: T;
next: Node<T> | null = null;
constructor(val: T) {
this.val = val;
}
}
class MyLinkedList<T> {
private head: Node<T> | null = null;
private size: number = 0;
private getNode(index: number): Node<T> | null {
let current = this.head;
let i = 0;
while (i++ < index && current) {
current = current.next;
}
return current;
}
// int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
get(index: number): any {
if (index < 0 || index >= this.size) {
return -1;
}
return this.getNode(index)?.val ?? null;
}
addAtHead(val: T): void {
const newNode = new Node(val);
newNode.next = this.head;
this.head = newNode;
this.size++;
}
addAtTail(val: T): void {
const newNode = new Node(val);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
// current此时,肯定是指向最后一个节点
current.next = newNode;
}
this.size++;
}
addAtIndex(index: number, val: T): boolean {
if (index < 0 || index > this.size) return false;
const newNode = new Node(val);
if (index === 0) {
newNode.next = this.head;
this.head = newNode;
} else {
let previous = this.getNode(index - 1);
// index === position
newNode.next = previous?.next ?? null;
previous!.next = newNode;
}
this.size++;
return true;
}
deleteAtIndex(index: number): T | null {
if (index < 0 || index >= this.size) return null;
// 删除元素
// 判断是否是删除第一个节点
let current = this.head;
if (index === 0) {
this.head = this.head?.next ?? null;
} else {
// 重构
let previous = this.getNode(index - 1);
// index === position
previous!.next = previous?.next?.next ?? null;
}
this.size--;
return current?.val ?? null;
}
}
export {};
head
,我们想删除它其中的一个节点 node
。给你一个需要删除的节点 node
。你将 无法访问 第一个节点 head
。
链表的所有值都是 唯一的,并且保证给定的节点 node
不是链表中的最后一个节点。
删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:
node
前面的所有值顺序相同。node
后面的所有值顺序相同。自定义测试:
head
和要给出的节点 node
。node
不应该是链表的最后一个节点,而应该是链表中的一个实际节点。class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
function deleteNode(node: ListNode | null): void {
node!.val = node!.next!.val;
node!.next = node!.next!.next;
}
export {};
head
,请你反转链表,并返回反转后的链表。class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
function reverseList(head: ListNode | null): ListNode | null {
if (!head || !head.next) return head;
const newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}