什么是算法
“算法” Algorithm,是计算机的一个术语,表示解决一个问题的方法和步骤。
什么是数据结构
储存信息的方式???
算法(描述方法)和数据结构(描述组织)之间存在非常紧密的联系。
简单来说,对信息的存储方式就是数据结构关心的事,对信息的处理方式就是算法关心的事。
算法的复杂度
衡量不同算法之间的优劣主要是通过「时间」和「空间」两个维度去考量:
- 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
- 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述
时间复杂度
时间复杂度是指执行这个算法所需要的计算工作量,其复杂度反映了程序执行时间「随输入规模增长而增长的量级」,在很大程度上能很好地反映出算法的优劣与否
Ο(1)<Ο(log n)<Ο(n)<Ο(nlog n)<Ο(n2)<Ο(n3)<…<Ο(2^n)<Ο(n!)
算法复杂度只是描述算法的增长趋势,并不能说一个算法一定比另外一个算法高效,如果常数项过大的时候也会导致算法的执行时间变长
空间复杂度
空间复杂度主要指执行算法所需内存的大小,用于对程序运行过程中所需要的临时存储空间的度量
数据结构
堆
堆的实现通过构造二叉堆(binary heap),实为二叉树的一种;
由于其应用的普遍性,当不加限定时,均指该数据结构的这种实现。
这种数据结构具有以下性质。
任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。堆总是一棵完全树。
即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
常见的堆有二叉堆、斐波那契堆等。
- 堆在本质上是一种
二叉树 - 但是它是用
数组的形式来描述二叉树 - 按照二叉树的
每一排依次进行序号的排序 - 且堆在
非最后子叶的情况下不能只有一个元素 - 对序号为
k的节点 它的左节点序号2k+1它的右节点序号2k+2它的父节点的序号是k/2向下取整
最大堆
- 父节点总大于子节点
- 顶部是最大值
最小堆
- 父节点总小于子节点
- 顶部是最小值
最大堆化 max-heapify
维护
最大堆性质的过程 和子节点不断比较将最大值和自己交换
最小堆化 min-heapify
维护
最小堆性质的过程
//1.初始化变量
//2.开始创建堆
//3.实现最大堆化
// 对序号为 `k` 的节点 它的左节点序号`2k+1` 它的右节点序号`2k+2`
class MaxHeap{
//1.初始化变量
constructor(data, max=10000){
this.list = new Array(max) //堆的最大值
for(let i = 0 ; i < data.length; i++){
this.list[i] = data[i]
}
this.heap_length = data.length //堆中真实元素个数
this.build(); //开始建堆
}
//2.开始创建堆
build(){
//首先获取最后一个元素的序号
//向上取整
let ind = Math.floor(this.heap_length / 2) + 1;
while (ind >= 0){
this.max_heapify(ind--) //树形结构 由下往上
}
}
//3.实现最大堆化
max_heapify(index){
//index为父级的序号
let parent_ind = index
//获取最大子元素序号,默认左子元素为最大
let max_child_ind = parent_ind * 2 +1;
while(max_child_ind <= this.heap_length -1){
//判断1: 子与子的大小关系
if(max_child_ind < this.heap_length - 1 && this.list[max_child_ind] < this.list[max_child_ind +1]){
//如果左子元素小于右子元素 将最大序号指向右子元素
max_child_ind = max_child_ind + 1
}
//判断2:子与父的大小关系
if(this.list[parent_ind] >= this.list[max_child_ind]){
//如何本身父元素大于子元素 不做处理 直接break
break;
}else{
//判断3:父元素小于子元素 将父元素与子元素进行交换
let tamp = this.list[parent_ind]
this.list[parent_ind] = this.list[max_child_ind];
this.list[max_child_ind] = tamp
}
}
}
}
//调用
const data = [12,15,2,4,5,6,77,8,99,100];
const test = new MaxHeap(data)
console.log(test)
- 先获取父节点,对父节点从下到上遍历排序;
- 排序原则,先默认左子节点为最大值,和右子节点比较得出子节点最大值,再用子节点最大值和父节点比较;
栈
栈是一种特殊的线性表,仅能在线性表的一端操作, 栈顶允许操作,栈底不允许操作。
栈的特点是:先进后出,或者说是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈。
function Stack(){
let items = []
// 添加元素到栈顶
this.push = function(element){
items.push(element)
}
// 栈的后进先出的原则,从栈顶出栈
this.pop = function(){
return items.pop();
}
// 查看栈顶的元素,访问数组最后一个元素
this.peek = function(){
return items[items.length - 1]
}
// 检查栈是否为空
this.isEmpty = function(){
return items.length == 0
}
//返回栈的长度,栈的长度就是数组的长度
this.size = function() {
return items.length
}
//清空栈
this.clear = function(){
items = []
}
// 打印栈元素
this.print = function(){
console.log(items.toString())
}
}
//使用
let stack = new Stack()
stack.push(1)
stack.push(2)
stack.push(3)
console.log(stack.peek()) // 3
console.log(stack.isEmpty()) //false
console.log("size:"+stack.size()) // 3
stack.pop()
stack.print();
队列
队列与栈一样,也是一种线性表,不同的是,队列可以在一端添加元素,在另一端取出元素,也就是:先进先出。从一端放入元素的操作称为入队,取出元素为出队。
队列的特点是是**"FIFO,即先进先出(First in, first out)"** 。 数据存取时**"从队尾插入,从队头取出"**。
"与栈的区别:栈的存入取出都在顶部一个出入口,而队列分两个,一个出口,一个入口" 。
//入队
//出队
class Queue{
constructor(max=10000){
this.max = max; // 队列最大值
this.data = new Array(max) //初始化空队列
this.p = 0; //入队指针
this.q = 0; //出队指针
this.size = 0 //初始化队列大小
}
//入队
enqueue(item){
if(this.size >= this.max){
throw new Error("队列溢出")
}
this.data[this.p++] = item; //入队指针指向下一个地址
this.size++ //队列大小+1
if(this.p >= this.max){
//入队指针到头,重新回到队列初始位置,形成一个闭环
this.p = 0
}
}
//出队
dequeue(){
if(this.size === 0){
throw new Error("队列下溢");
}
let target = this.data[this.q++];
this.size--
if(this.q >= this.max){
//出队指针到头,重新回到队列初始位置,形成一个闭环
this.q = 0
}
return target
}
}
//测试
let p = new Queue()
p.enqueue(1)
p.enqueue(2)
p.enqueue(3)
p.enqueue(4)
p.enqueue(5)
p.enqueue(6)
console.log(p.dequeue())
console.log(p)
链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
特点:
定点删除/插入元素
链表一共有两个操作
- 插入:将一个新的节点插入链表
- 删除:将一个节点从链表中删除
//初始化
function init(){
return {
data: "start",
next: null
}
}
//插入(两种)
function insert(node, newNode, data){
while(node){
//插入到指定节点后面
if(data && node.data === data){
if(node.next){
newNode.next = node.next
}
node.next = newNode
return
}
//直接插入到最后面
if(!data && node.next === null){
node.next = newNode
return
}
node = node.next
}
}
//删除 delete(node,2)
// {'data':'start','next':{'data':2, 'next':null}}
function delete_(node, data){
let parrent = node;
node = node.next
while(node){
if(node.data === data){
if(parent){
parent.next = node.next
return
}else{
return
}
}
parent = node
node = node.next
}
}
//调用
let node = init()
insert(node, {data: 2, next: null})
insert(node, {data: 3, next: null})
insert(node, {data: 4, next: null})
delete_(node, 3)
console.log(node)
矩阵
矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合
矩阵看起来就是一组排列起来的向量,其中向量的维数是矩阵的行,向量的个数,叫做矩阵的列。在下面的注释中,就是一个3x3的矩阵:
///
/// | 5 6 7 |
/// | 2 4 3 |
/// | 1 9 8 |
///
但是,矩阵的行和列并不一定是相同的,并且,我们管这种行列个数相同的矩阵,叫做方阵。
矩阵转置算法
//1.转置矩阵行数是原矩阵的列数,转置矩阵的列数是原始矩阵的行数
//2.转置矩阵下标(i,j)的元素,转置后的(j,i)的元素
function reverseMatrix(sourceArr){
var reversedArr = [];
sourceArr.reverse();
//原矩阵列数
for(var i =0; i < sourceArr[0].length ; i++){
reversedArr[i] = []
for(var j=0; j < sourceArr.length; j++){
reversedArr[i][j] = sourceArr[j][i]
}
}
return reversedArr;
}
//调用
var tArr = [
[1,2,3],
[4,5,6],
[7,8,9]
]
var testArr = reverseMatrix(tArr)
console.log(JSON.stringify(testArr))
二叉树
1) 数据结构【二叉树】
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。在日常的应用中,我们讨论和用的更多的是树的其中一种结构,就是二叉树。
特点
每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。二叉树中每一个节点都是一个对象,每一个数据节点都有三个指针,分别是指向父母、左孩子和右孩子的指针。每一个节点都是通过指针相互连接的。相连指针的关系都是父子关系。
二叉树的五种基本形态
空二叉树
只有一个根结点
根结点只有左子树
根结点只有右子树
根结点既有左子树又有右子树
拥有三个结点的普通树只有两种情况:两层或者三层。但由于二叉树要区分左右,所以就会演变成五种形态;
特殊二叉树
**1)**满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
2) 完全二叉树
完全二叉树是指最后一层左边是满的,右边可能满也可能不满,然后其余层都是满的。一个深度为k,节点个数为 2^k - 1(2的*N次幂) 的二叉树为满二叉树(完全二叉树)。就是一棵树,深度为k,并且没有空位。
叶子结点只能出现在最下两层。
最下层的叶子一定集中在左部连续位置。
倒数第二层,若有叶子结点,一定都在右部连续位置。
如果结点度为1,则该结点只有左孩子。
同样结点树的二叉树,完全二叉树的深度最小。
注意:满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
完全二叉树算法如下:
二叉树的性质
二叉树的性质一:在二叉树的第i层上至多有2^(i-1)个结点(i>=1)
二叉树的性质二:深度为k的二叉树至多有2^k-1个结点(k>=1)
二叉树的顺序存储结构
二叉树的顺序存储结构就是用一维数组存储二叉树中的各个结点,并且结点的存储位置能体现结点之间的逻辑关系。
二叉树的遍历
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树的遍历有三种方式,如下:
(1)前序遍历(DLR),首先访问根结点,然后遍历左子树,最后遍历右子树。简记根-左-右。
(2)中序遍历(LDR),首先遍历左子树,然后访问根结点,最后遍历右子树。简记左-根-右。
(3)后序遍历(LRD),首先遍历左子树,然后遍历右子树,最后访问根结点。简记左-右-根。
前序遍历:
若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
中序遍历:
若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
后序遍历:
若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点。
//定义节点
class Node{
constructor(data){
this.root = this; //根节点
this.data = data; //当前数据
this.left = null; //左子树
this.right = null; //右子树
}
}
//创建二叉树搜索树
class BinarySearchTree{
constructor(){
this.root = null;
}
//插入节点
insert(data){
const newNode = new Node(data);
const insertNode = (node, newNode) =>{
//新加入值小于node节点
if(newNode.data < node.data){
//向左子树插入
if(node.left === null){
node.left = newNode
}else{
insertNode(node.left, newNode)
}
}else{
//向右子树插入
if(node.right === null){
node.right = newNode
}else{
insertNode(node.right, newNode)
}
}
}
//根节点
if(!this.root){
this.root = newNode
}else{
insertNode(this.root, newNode)
}
}
//前序遍历
preOrder(){
let backs = [];
const preOrderNode = (node, callback) =>{
if(node !== null){
//先访问根节点
backs.push(callback(node.data))
//左子树
preOrderNode(node.left, callback);
//右子树
preOrderNode(node.right, callback)
}
}
preOrderNode(this.root, callback)
function callback(v){
return v
}
return backs
}
//中序遍历
inOrder(){
let backs = [];
const inOrderNode = (node, callback) =>{
if(node !== null){
inOrderNode(node.left, callback);
backs.push(callback(node.data));
inOrderNode(node.right, callback);
}
}
inOrderNode(this.root, callback)
function callback(v){
return v
}
return backs
}
//后序遍历
postOrder(){
let backs = [];
const postOrderNode = (node,callback) =>{
if(node !== null){
postOrderNode(node.left, callback);
postOrderNode(node.right, callback);
backs.push(callback(node.data));
}
}
postOrderNode(this.root,callback);
function callback(v){
return v
}
return backs
}
//最小值
getMin(node){
const minNode = node =>{
return node ? (node.left ? minNode(node.left) : node) : null;
}
return minNode (node || this.root)
}
//最大值
getMax(node){
const maxNode = node =>{
return node ? (node.right ? maxNode(node.right) : node) : null;
}
return maxNode(node || this.root)
}
//查找特定值
find(data){
const findNode = (node, data) =>{
if(node === null) return false;
if(node.data === data) return node;
return findNode((data < node.data)? node.left :node.right,data)
}
return findNode(this.root, data)
}
}
//调用
const tree = new BinarySearchTree();
tree.insert(11);
tree.insert(7);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
console.log(tree)
//前序遍历
console.log("前序遍历",tree.preOrder());
//中序遍历
console.log("中序遍历",tree.inOrder());
//后序遍历
console.log("后序遍历",tree.postOrder());
//最小值
console.log("最小值",tree.getMin())
//最大值
console.log("最大值",tree.getMax())
//查找特定值
console.log("查找特定值",tree.find(10))
二叉查找树
二叉查找树(Binary Search Tree,简称BST),(又:二叉搜索树,二叉排序树) 它是一种基于节点的二叉树数据结构,具有以下特性:
- 节点的左子树仅包含值小于节点值的节点。
- 节点的右子树仅包含值大于节点值的节点。
- 左右子树也必须是二叉查找树。
平衡二叉树
平衡二叉树 (AVL) 树是一种自平衡二叉查找树 (BST),并且其中所有节点的左右子树的高度差不能超过 1。
红黑树
红黑树是一种自平衡二叉搜索树,每个节点都有一个额外的位置用来存储节点的颜色(红色或黑色)。这些颜色用于确保树在插入和删除过程中保持平衡。
红黑树的特性
- 每个节点要么是红色,要么是黑色。
- 根节点永远是黑色的。
- 所有的叶子节点都是空节点(即null),并且是黑色的。
- 没有两个相邻的红色节点(红色节点不能有红色父节点或红色子节点)。
- 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。