图灵得主尼古拉斯·沃斯曾经说过
算法+数据结构=程序数据结构和算法是计算机科学的基础,在设计和分析程序的时候起到了重要作用,正确的使用数据结构和算法可以提高程序的性能和可维护性
数据结构
- 数据结构就是在计算机中,存储和组织数据的方式
- 常见的数据结构有数组,链表,栈,队列,树,图...等等
- 用一个优秀的数据结构,能够更快的帮你解决问题,优化程序性能
1.线性结构
- 是由n个元素组成的a[0],a[1],a[2]...a[n-1]组成的有序数列
- 数组是一个线性结构
- 链表是一个线性结构
- 栈是一个受限的线性结构
- 队列是一个受限的线性结构
接下来用代码实现一个栈结构
// 栈结构 : 后进入先出
interface IStack<T> {
push: (el: T) => void
pop: () => void
peek: () => T | undefined
isEmpty: () => boolean
size: () => number
}
class DJArrayStack<T> implements IStack<T> {
private data: T[] = []
push(element: T) {
this.data.push(element)
}
pop() {
return this.data.pop()
}
peek(): T | undefined {
if (this.data.length) {
return this.data[this.data.length - 1]
}
return undefined
}
isEmpty(): boolean {
return !this.data.length
}
size(): number {
return this.data.length
}
}
栈结构相关面试题
- 十进制转二进制
function decimalToBinary(num: number): string {
// 4 => 100
if (num == 0) {
return '0'
}
const arr = new DJArrayStack<number>()
while (num > 0) {
arr.push(num % 2)
num = Math.floor(num / 2)
}
let res = ''
while (!arr.isEmpty()) {
res += arr.pop()
}
return res
}
- 有效的括号
const parentheses = '[]({})'
function checkParentheses(par: string): boolean {
const stack = new DJArrayStack<string>()
for (let i = 0; i < par.length; i++) {
const element = par[i]
switch (element) {
case '(':
stack.push(')')
break
case '[':
stack.push(']')
break
case '{':
stack.push('}')
break
default:
if (element != stack.pop()) return false
break
}
}
return stack.isEmpty()
}
2.队列结构
- 先进先出
- node的任务队列
class DJArrayQueue<T> {
private data: T[] = []
enqueue(...args: T[]) {
this.data.push(...args)
}
dequeue(): T | undefined {
return this.data.shift()
}
peek(): T | undefined {
if (this.data.length == 0) {
return undefined
}
return this.data[0]
}
get isEmpty() {
return !this.data.length
}
get size() {
return this.data.length
}
}
相关面试题
- 击鼓传花
const names = ['0', '1', '2', '3', '4']
function getJgchLastIndex(names: string[], outIndex: number) {
const queue = new DJArrayQueue<string>()
queue.enqueue(...names)
let i = 0
while (queue.size > 1) {
i++
let front = queue.dequeue()
if (i == outIndex) {
i = 0
} else {
queue.enqueue(front)
}
}
return queue.peek()
}
- 约斯夫环 用动态规划
function lastRemaining(n: number, m: number): number {
let ans = 0
return ans
}
3. 链表的实现
- 链表分为单向链表和双向链表
class DJNode<T> {
value: T
next: DJNode<T> | null = null
constructor(val: T) {
this.value = val
}
}
class DJLinkedList<T> {
head: DJNode<T> | null = null
private size: number = 0
private _lastNode: DJNode<T> | null = null
get length() {
return this.size
}
/**向链表尾部添加一个新的项 */
append(element: T) {
const newNode = new DJNode<T>(element)
if (this.size == 0) {
this.head = newNode
} else {
this._lastNode.next = newNode
}
this._lastNode = newNode
this.size++
}
/**向链表的特定位置插入一个新的项 */
insert(position: number, element: T) {
const newNode = new DJNode<T>(element)
if (position == 0 || this.size == 0) {
newNode.next = this.head
this.head = newNode
} else if (position >= this.size) {
this._lastNode.next = newNode
this._lastNode = newNode
} else {
const preNode = this.getNode(position - 1)
newNode.next = preNode.next
preNode.next = newNode
this.size++
}
}
/**获取对应位置的元素 */
get(position: number): T | undefined {
const curNode = this.getNode(position)
return curNode.value
}
/**返回元素所在链表中的索引,如果链表中没有该元素则返回-1 */
indexOf(element: T) {
if (this.size == 0) {
return -1
}
let index = 0
let curNode = this.head
while (curNode.next) {
if (element === curNode.value) {
break
}
index++
curNode = curNode.next
}
return index
}
/**更新某个位置的元素 */
update(position: number, element: T) {
const ans = this.getNode(position)
if (ans != null) {
ans.value = element
}
}
/**移除特定位置 */
removeAt(position: number) {
if (position >= this.size) {
return
}
if (position == 0) {
this.head = this.head.next
}
const preNode = this.getNode(position - 1)
preNode.next = preNode.next.next
this.size--
}
/**移除特定元素*/
remove(element: T) {
const position = this.indexOf(element)
if (position >= 0) {
this.removeAt(position)
}
}
isEmpty() {
return this.size > 0
}
/**获取特定位置的Node */
private getNode(position: number): DJNode<T> | null {
let index = 0
let curNode = this.head
while (index++ < position && curNode) {
curNode = curNode.next
}
return curNode
}
/**打印 */
traverse() {
let curNode = this.head
let res = ''
while (curNode) {
res += curNode.value + ' -> '
curNode = curNode.next
}
console.log(res)
}
}
4. 哈希表
- 本质就是将下标值先哈希化
- 哈希表的实现还是利用了数组
- 解决下标值问题
- 1.链地址法解析
链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一个链条 * 这个链条使用的数据结构通常是数组或者是链表 * 如果是链表, 就是每个数组的单元中存储一个链条,一旦有重复,则把重复的元素加入到该链条的尾部或者首部 * 当查询时,先根据哈希化后的下标值找到对应的位置,再取出链表,依次查询寻找的数据 那选择数组还是链表呢? * 数组或者链表这里都可以,效率上是差不多的 * 因为根据哈希化的index找到这个数组或者链表的时候,通常就会使用线性查找,这个时候数组和链表的效率差不多 * 当然在某些实现中,会将新插入的数据放在数组或链表的最前面,因为觉得新插入的数据取出的概率更大 * 这种情况最好是用链表,因为插入数据链表的O(1)比数组O(n)要好 * 总之还是看业务需求,这里数组或链表都是可以的- 2.开放地址法
开发地址的主要做法是寻找空白的位置来添加重复的数据 * 解决重复index又有三种方法 * 1.线性探测 * 2.二次探测 * 3.再哈希
- 链地址法效率是要好于开放地址法的
- Java的HashMap就是用的链地址法
核心是哈希函数 -> 而哈希函数的实现使用霍纳法则来减少生成hashCode所需要经历的乘法+加法,主要是乘法的减少,乘法对比加减法还是比较消耗性能
代码实现:
/**
* 这个函数返回这个key在数组中的下标值
* @param key 要保存的key
* @param max 哈希表中数组的长度
*/
function hashFunc(key: string, max: number): number {
let hashCode = 0
const length = key.length
for (let i = 0; i < length; i++) {
hashCode = 31 * hashCode + key.charCodeAt(i)
}
let index = hashCode % max
return index
}
//哈希表
class HashTable<T = any> {
storage: [string, T][][] = []
private length = 7
private count = 0
private hashCode(key: string, max: number): number {
return hashFunc(key, max)
}
put(key: string, value: T) {
//获取索引值
const index = this.hashCode(key, this.length)
let bucket = this.storage[index]
if (!bucket) {
bucket = []
this.storage[index] = bucket
}
let isUpdate = false
for (let i = 0; i < bucket.length; i++) {
const tuple = bucket[i]
const tupleKey = tuple[0]
if (tupleKey === key) {
tuple[1] = value
isUpdate = true
break
}
}
if (!isUpdate) {
bucket.push([key, value])
this.count++
const loadFactor = this.count / this.length
if (loadFactor > 0.75) {
this.resize(this.length * 2)
}
}
}
get(key: string): T | undefined {
const index = this.hashCode(key, this.length)
const bucket = this.storage[index]
if (!bucket) {
return undefined
}
for (let i = 0; i < bucket.length; i++) {
const [tupleKey, tupleValue] = bucket[i]
if (tupleKey === key) {
return tupleValue
}
}
return undefined
}
delete(key: string): boolean {
const index = this.hashCode(key, this.length)
const bucket = this.storage[index]
if (!bucket) {
return false
}
for (let i = 0; i < bucket.length; i++) {
const [tupleKey, _] = bucket[i]
if (tupleKey === key) {
bucket.splice(i, 1)
this.count--
const loadFactor = this.count / this.length
if (loadFactor < 0.25) {
this.resize(Math.floor(this.length / 2))
}
return true
}
}
return false
}
private resize(newLength: number) {
if (newLength < 7) newLength = 7
if (this.length === newLength) return
const primeNum = this.getNextPrimeNumber(newLength)
console.log('新的容量 : ', primeNum)
this.length = primeNum
const oldStorage = this.storage
this.storage = []
this.count = 0
oldStorage.forEach((bucket) => {
if (!bucket) {
return
}
for (let i = 0; i < bucket.length; i++) {
const [key, value] = bucket[i]
this.put(key, value)
}
})
}
// 获取下一个质数
private getNextPrimeNumber(num: number): number {
let newPrime = num
while (!this.isPrimeNumber(newPrime)) {
newPrime++
}
return newPrime
}
//判断是否质数
private isPrimeNumber(num: number): boolean {
if (num < 2) {
return false
}
const mid = Math.sqrt(num)
for (let i = num - 1; i >= mid; i--) {
const remainder = num % i
if (remainder === 0) {
return false
}
}
return true
}
}
const map1 = new HashTable()
map1.put('cab', 111)
map1.put('nab', 222)
map1.put('nba', 333)
map1.put('cba', 444)
map1.put('ddd', 444)
map1.put('fff', 444)
map1.put('ggg', 444)
5. 树结构
所有的树结构本质上都可以使用二叉树把它表述出来的 - 因为用儿子-兄弟这个表示方法,然后旋转45度,然后就是一个二叉树
二叉树的特性
- 一颗二叉树的第i层的最大节点数为: 2^(i- 1), i>=1
- 深度为K的二叉树有最大的节点总数为: 2^k - 1, k>=1
- 对于任何非空二叉树T,若n0表示叶子节点的个数,n2表示度为2的非叶子节点个数,那么二者满足关系 n0 = n2 + 1
完美二叉树(也叫做满二叉树)
- 假设深度为K,则除了第K层的叶子节点,其他节点都是度为2
完全二叉树 (堆的本质就是一颗完全二叉树)
- 假设深度为K,则除了二叉树第K层外,其他节点都是度为2
- 且最后一层从左往右的叶节点是连续存在,只缺右侧若干节点
- 完美二叉树是特殊的完全二叉树
二叉树的存储
-
数组: 完全二叉树可以用数组来存,非完全二叉树用数组存储需要先补全成完全二叉树,会造成内存的浪费
-
链表: 比较常用
二叉搜索树BST
不用空的情况下,要满足下面条件
- 非空左子树的所有键值小于其根节点的键值
- 非空右子树的所有键值大于其根节点的键值
- 左右子树本身也都是二叉搜索树
二叉搜索树 实现
//二叉搜索树 实现
class TreeNode<T> {
value: T | null = null
left: TreeNode<T> | null = null
right: TreeNode<T> | null = null
parent: TreeNode<T> | null = null
constructor(value: T) {
this.value = value
}
get isLeft(): boolean {
return this.parent && this.parent.left === this
}
get isRight(): boolean {
return this.parent && this.parent.right === this
}
}
class BSTree<T> {
private root: TreeNode<T> | null = null
insert(value: T) {
const node = new TreeNode<T>(value)
if (!this.root) {
this.root = node
return
}
let parentNode = this.root
while (parentNode) {
if (node.value > parentNode.value) {
//找右边
if (parentNode.right == null) {
parentNode.right = node
break
}
parentNode = parentNode.right
} else {
//找左边
if (parentNode.left == null) {
parentNode.left = node
break
}
parentNode = parentNode.left
}
}
}
/**
* 前序遍历
* - 先访问根节点
* - 再访问左子树
* - 再访问右子树
*/
preOrderTraverse() {
if (!this.root) {
return
}
const ans = []
function traverse(node: TreeNode<T>) {
if (!node) {
return
}
ans.push(node.value)
traverse(node.left)
traverse(node.right)
}
traverse(this.root)
console.log(ans)
}
/**
* 中序遍历
* - 先访问左子树
* - 再访问根节点
* - 再访问右子树
*/
inOrderTraverse() {
if (!this.root) {
return
}
const ans = []
function traverse(node: TreeNode<T>) {
if (!node) {
return
}
traverse(node.left)
ans.push(node.value)
traverse(node.right)
}
traverse(this.root)
console.log(ans)
}
/**
* 后序遍历
* - 先访问左子树
* - 再访问右子树
* - 再访问根节点
*/
postOrderTraverse() {
if (!this.root) {
return
}
const ans = []
function traverse(node: TreeNode<T>) {
if (!node) {
return
}
traverse(node.left)
traverse(node.right)
ans.push(node.value)
}
traverse(this.root)
console.log(ans)
}
/**
* 层序遍历
* - 一层一层的遍历
* - 用队列来实现
*/
levelOrderTraverse() {
if (!this.root) {
return
}
const ans = []
const queue = new DJArrayQueue<TreeNode<T>>()
queue.enqueue(this.root)
while (!queue.isEmpty) {
const node = queue.dequeue()
ans.push(node.value)
if (node.left) {
queue.enqueue(node.left)
}
if (node.right) {
queue.enqueue(node.right)
}
}
console.log(ans)
}
getMaxValue(): T | null {
if (!this.root) {
return null
}
let node = this.root
while (node.right) {
node = node.right
}
return node.value
}
getMinValue(): T | null {
if (!this.root) {
return null
}
let node = this.root
while (node.left) {
node = node.left
}
return node.value
}
private searchNode(value: T): TreeNode<T> | null {
let node = this.root
while (node) {
if (value === node.value) {
return node
}
const parent = node
if (value > node.value) {
node = node.right
} else if (value < node.value) {
node = node.left
}
node.parent = parent
}
return null
}
// 搜索到特定的值
search(value: T): boolean {
//非递归
// let node = this.root
// while (node) {
// if (value > node.value) {
// node = node.right
// } else if (value < node.value) {
// node = node.left
// } else {
// return true
// }
// }
// return false
//递归
function findValue(node: TreeNode<T>): boolean {
if (!node) {
return false
}
if (value === node.value) {
return true
}
if (value > node.value) {
return findValue(node.right)
} else {
return findValue(node.left)
}
}
return findValue(this.root)
}
//删除功能
delete(value: T): boolean {
//首先要找到这个元素
const node = this.searchNode(value)
if (!node) {
return false
}
// console.log(`要删除的元素${node.value} - 父节点=${node.parent.value} - 是左边吗=${node.isLeft}`)
//一共有3种情况
//1.删除的是叶子节点
if (node.left == null && node.right == null) {
if (node === this.root) {
this.root = null
} else if (node.isLeft) {
node.parent.left = null
} else {
node.parent.right = null
}
} else if (node.left == null) {
//2.删除的节点 只有一个子节点
if (node == this.root) {
this.root = node.right
} else if (node.isLeft) {
node.parent.left = node.right
} else {
node.parent.right = node.right
}
} else if (node.right == null) {
if (node == this.root) {
this.root = node.left
} else if (node.isLeft) {
node.parent.left = node.left
} else {
node.parent.right = node.left
}
} else {
//3.删除的节点 有2个子节点
// 这个关键在于找到删除节点的前驱或者是后继节点
// 1. 前驱: 就是比删除节点稍微小一点的数字
// 2. 后继: 就是比删除节点稍微大一点的数字
// 找前驱
const preNode = findPreMax(node)
if (node.left !== preNode) {
preNode.parent.right = preNode.left
preNode.left = node.left
}
preNode.right = node.right
if (node === this.root) {
this.root = preNode
} else if (node.isLeft) {
node.parent.left = preNode
} else {
node.parent.right = preNode
}
}
function findPreMax(delNode: TreeNode<T>): TreeNode<T> {
let preNode = delNode.left
while (preNode.right) {
const parent = preNode
preNode = preNode.right
preNode.parent = parent
}
return preNode
}
return true
}
print() {
btPrint(this.root)
}
}
const bstree = new BSTree<number>()
bstree.insert(30)
bstree.insert(22)
bstree.insert(24)
bstree.insert(26)
bstree.insert(23)
bstree.insert(33)
bstree.insert(13)
bstree.insert(6)
bstree.insert(16)
bstree.insert(15)
bstree.insert(34)
// bstree.print()
bstree.delete(30)
6. 图结构
历史: 欧拉和七桥问题
-
图结构是一种与树结构有些相似的数据结构
-
图论是数学的一个分支,并且在数学的概念上,树是图的一种
-
它以图为研究对象,研究顶点和边组成的图形的数学理论和方法
-
主要研究的目的是事物之间的关系, 顶点代表事物,边代表两个事物之前的关系
-
历史: 欧拉和七桥问题
- 连通图可以一笔画的必要条件
-
- 奇点的数目不是0个就是2个
-
- 想要一笔画成, 必须中间都是偶数点 也就是说: 有来路必有另一条去路,奇点只能在两端,因此任何图能一笔画成,奇点要么没有,要么在两端
图结构的表示 - 1.邻接矩阵 : - 是用二维数组来表示2个顶点之间有没有联系,有联系显示1 没联系显示0 - 但是这样在稀疏图的时候,用有大量的0来保存在内存中,浪费空间
- 2.邻接表
- 邻接表由图中 每个顶点以及和顶点相邻的顶点列表组成
- 每个列表有很多的方式来存储: 数组/链表/字典(哈希表)都可以
图的访问: 1.广度优先 BFS 基于队列 2.深度优先 DFS 基于栈或者使用递归
class Graph<T> {
//顶点
private vertecs: T[] = []
//路径
private edges: Map<T, T[]> = new Map()
//添加顶点
addVertex(vertex: T) {
this.vertecs.push(vertex)
this.edges.set(vertex, [])
}
//添加路径
addEdge(vertex: T, edge: T) {
this.edges.get(vertex).push(edge)
}
traversal() {
this.vertecs.forEach((vertex) => {
const edges = this.edges.get(vertex)
console.log(`顶点 ${vertex} 路径有${edges.join(' ')}`)
})
}
//广度优先
bfs() {
//用队列实现
const queue = new DJArrayQueue<T>()
queue.enqueue(this.vertecs[0])
const visited = new Set<T>()
visited.add(this.vertecs[0])
while (!queue.isEmpty) {
const vertex = queue.dequeue()
console.log(vertex)
const neighbor = this.edges.get(vertex)
if (!neighbor) continue
neighbor.forEach((n) => {
if (!visited.has(n)) {
visited.add(n)
queue.enqueue(n)
}
})
}
}
//深度优先
dfc() {
//使用栈来实现
const stack = new DJArrayStack<T>()
stack.push(this.vertecs[0])
const visited = new Set<T>()
visited.add(this.vertecs[0])
while (!stack.isEmpty) {
const pop = stack.pop()
console.log(pop)
const neighbor = this.edges.get(pop)
if (!neighbor) continue
for (let i = neighbor.length - 1; i >= 0; i--) {
if (!visited.has(neighbor[i])) {
visited.add(neighbor[i])
stack.push(neighbor[i])
}
}
}
}
}
const graph = new Graph()
for (let i = 65; i < 71; i++) {
graph.addVertex(String.fromCharCode(i))
}
graph.addEdge('A', 'B')
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('C', 'D')
graph.addEdge('C', 'G')
graph.addEdge('D', 'G')
graph.addEdge('D', 'H')
graph.addEdge('B', 'E')
graph.addEdge('B', 'F')
graph.addEdge('E', 'I')
graph.traversal()
算法
- 算法就是指解决问题的方法和步骤
- 常见的算法有快速排序,冒泡排序,二分查找,最短路径算法...等等
二分查找
// 二分查找
function binarySearch(arr: number[], num: number) {
let left = 0
let right = arr.length - 1
while (left <= right) {
let index = Math.floor((right - left) / 2 + left)
if (num > arr[index]) {
left = index + 1
} else if (num < arr[index]) {
right = index - 1
} else {
return index
}
}
return -1
}