数据结构与算法(JS)

数据结构:

物理结构:顺序存储(把数据存放在地址连续的储存单元里)、链式存储(放在任意储存单元)

逻辑结构:集合 线性 树状 网络

栈(LIFO:last in first out):

只能在一端进行插入或者删除,后进先出【受限的线性结构】

栈应用

函数栈,先执行funA,再压入funB执行,根据funB的返回值地址回溯到funA继续执行。

十进制转二进制

栈常见的操作:
  • push(element):添加一个新元素到栈顶位置;

  • pop():移除栈顶的元素,同时返回被移除的元素;

  • peek():返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返回它);

  • isEmpty():如果栈里没有任何元素就返回true,否则返回false;

  • size():返回栈里的元素个数。这个方法和数组的length属性类似;

  • toString():将栈结构的内容以字符串的形式返回。

用数组封装一个栈的实现:
function Stack(){//封装栈类

            this.items=[]

            Stack.prototype.push=function(element){//常选择直接挂在类上,而不是类的实例上
                this.items.push(element)
            }
            Stack.prototype.pop = () => {
                return this.items.pop()//出栈 数组的pop方法本来就是取出的最后一个元素
            }
            Stack.prototype.peek = () => {//查看栈顶元素
                return this.items[this.items.length - 1]
            }
            Stack.prototype.isEmpty = () => {//判断栈是否为空
                return this.items.length == 0 
            }
            Stack.prototype.size=()=>{//栈的长度
                return this.items.length
            }
            Stack.prototype.toString=()=>{//输出字符串形式的数组
                let str=''
                for(let i of this.items){
                    str=str+i+" "//字符串拼接
                }
                return str
            }
        }

        let s=new Stack()
        s.push(18)
        s.push(20)
        s.push(21)
        s.push(45)
        console.log(s.items)//打印数组
        s.pop()
        console.log(s.toString())//打印字符串形式数组元素
        console.log(typeof(s.toString()))//检验数据类型
队列(FIFO:first in first out):

一端添加,另一端删除,先进先出。后端(rear)添加,前端(front)删除。【受限的线性结构】

队列应用:
  • 打印队列:计算机打印多个文件的时候,需要排队打印;

  • 线程队列:当开启多线程时,当新开启的线程所需的资源不足时就先放入线程队列,等待CPU处理;

队列的常见操作:
  • enqueue(element):向队列尾部添加一个(或多个)新的项;

  • dequeue():移除队列的第一(即排在队列最前面的)项,并返回被移除的元素;

  • front():返回队列中的第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动(不移除元素,只返回元素信息与Stack类的peek方法非常类似);

  • isEmpty():如果队列中不包含任何元素,返回true,否则返回false;

  • size():返回队列包含的元素个数,与数组的length属性类似;

  • toString():将队列中的内容,转成字符串形式;

用数组封装一个队列的实现:

击鼓传花小游戏:

  //使用队列实现小游戏:击鼓传花,传入一组数据和设定的数字num,
        //循环遍历数组内元素,遍历到的元素为指定数字num时将该元素删除,直至数组剩下一个元素
    function game(list,num){

        let que=new Queue
        for(let i of list){
            que.enqueue(i)
        }//进队

        while(que.size()>1){
            for(i=0;i<num-1;i++){
                que.enqueue(que.dequeue())//先出队再进队
            }
            que.dequeue()//删除
        }
        console.log(que.size())
        let endP=que.front();//剩下的那位
        console.log(endP)

        let p=list.indexOf(endP)//取出索引
        return p

    }
    let arr=['ming','hong','li','liang','huang','www']
    console.log(game(arr,3))
优先级队列:
  • 每个元素不再只是一个数据,还包含数据的优先级;

  • 在添加数据过程中,根据优先级放入到正确位置;

链表:
单链表:

数组存在的缺点(插入删除复杂度高):

  • 数组的创建通常需要申请一段连续的内存空间(一整块内存),并且大小是固定的。所以当原数组不能满足容量需求时,需要扩容(一般情况下是申请一个更大的数组,比如2倍,然后将原数组中的元素复制过去)。

  • 在数组的开头或中间位置插入数据的成本很高,需要进行大量元素的位移。

链表的优势:

  • 链表中的元素在内存中不必是连续的空间,可以充分利用计算机的内存,实现灵活的内存动态管理

  • 链表不必在创建时就确定大小,并且大小可以无限地延伸下去。

  • 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多。

链表的缺点:

  • 链表访问任何一个位置的元素时,都需要从头开始访问(无法跳过第一个元素访问任何一个元素)。

  • 无法通过下标值直接访问元素,需要从头开始一个个访问,直到找到对应的元素。

  • 虽然可以轻松地到达下一个节点,但是回到前一个节点是很难的。

单链表中的常见操作:
  • append(element):向链表尾部添加一个新的项;

  • insert(position,element):向链表的特定位置插入一个新的项;

  • get(position):获取对应位置的元素;

  • indexOf(element):返回元素在链表中的索引。如果链表中没有该元素就返回-1;

  • update(position,element):修改某个位置的元素;

  • removeAt(position):从链表的特定位置移除一项;

  • remove(element):从链表中移除一项;

  • isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;

  • size():返回链表包含的元素个数,与数组的length属性类似;

  • toString():由于链表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值;

//insert方法
            LinkList.prototype.insert=function(position,data){
                if(position<0||position>this.length){return false}
                let iN=new Node(data)
                if(position==0){
                    iN.next=this.head//新节点的next为头节点指向的
                    this.head=iN//头节点指向新节点
                }else{
                    let index=0
                    // let previous=null......不需要双指针
                    let current=this.head
                    while(++index<position){
                        //previous=current
                        current=current.next
                    }
                    iN.next=current.next
                    current.next=iN    
                }
                this.length+=1
                return true
            }
//removeAt移除某位置的元素
            LinkList.prototype.removeAt=function(position){
                if(position < 0 || position >= this.length){return null}
                let current=this.head
                if(position==0){this.head=this.head.next}
                else{
                    
                    let index=0
                    let previous=null//加一个先前节点
                    while(index++<position){
                        previous=current//当current未指向下一节点时获取
                        current=current.next
                    }
                    previous.next=current.next
                }
                //this.length-=1
                return current.data
            }
双链表:

既可以从头遍历到尾,又可以从尾遍历到头。也就是说链表连接的过程是双向的,它的实现原理是:一个节点既有向前连接的引用,也有一个向后连接的引用

双向链表的缺点:

  • 每次在插入或删除某个节点时,都需要处理四个引用,而不是两个,实现起来会困难些;

  • 相对于单向链表,所占内存空间更大一些;(因为变量多?)

  • 但是,相对于双向链表的便利性而言,这些缺点微不足道。

双向链表常见的操作(方法):

  • append(element):向链表尾部添加一个新的项;

  • inset(position,element):向链表的特定位置插入一个新的项;

  • get(element):获取对应位置的元素;

  • indexOf(element):返回元素在链表中的索引,如果链表中没有元素就返回-1;

  • update(position,element):修改某个位置的元素;

  • removeAt(position):从链表的特定位置移除一项;

  • isEmpty():如果链表中不包含任何元素,返回trun,如果链表长度大于0则返回false;

  • size():返回链表包含的元素个数,与数组的length属性类似;

  • toString():由于链表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值;

  • forwardString():返回正向遍历节点字符串形式;

  • backwordString():返回反向遍历的节点的字符串形式;

doubleList.prototype.insert=(position,data)=>{
                let node=new Node(data)
                if(position<0||position>this.length){return false}//先进行越界判断哦
                if(this.length==0){    
                    this.head=node
                    this.tail= node
                }else{
                    if(position==0){
                        node.next=this.head
                        this.head.previous=node//这里不能直接写this.previous
                        this.head=node    
                    }else if(position==this.length){
                        node.previous=this.tail
                        this.tail.next=node
                        this.tail=node
                    }else{
                        let current=this.head
                        let index=0
                        while(index<position){
                            current=current.next
                            index+=1
                        }
                        node.previous=current.previous
                        current.previous.next=node
                        current.previous=node//当前节点的上一节点
                        node.next=current
                    }
                    this.length+=1
                    return true
                }
            }
doubleList.prototype.removeAt=function(position){
                if(position < 0 || position >= this.length){return null}
                let current=this.head//一定要写在最外面
                if(this.length==1){
                    this.head=null
                    this.tail=null
                    this.length=0
                }else{
                    if(position==0){
                        this.head.next.previous=null
                        this.head=this.head.next
                    }else if(position==this.length-1){
                        current=this.tail//指针改变前记录一下当前指针,便于返回
                        this.tail.previous.next=null
                        this.tail=this.tail.previous
                    }else{
                        let index=0
                        while(index<position){
                            index+=1
                            current=current.next
                        }
                        current.previous.next=current.next
                        current.next.previous=current.previous
                    }
                } 
                this.length-=1
                return current.data
            }
集合:

集合比较常见的实现方式是哈希表,这里使用JavaScript的Object类进行封装。

集合通常是由一组无序的不能重复的元素构成。

  • 数学中常指的集合中的元素是可以重复的,但是计算机中集合的元素不能重复。

集合是特殊的数组:

  • 特殊之处在于里面的元素没有顺序也不能重复

  • 没有顺序意味着不能通过下标值进行访问,不能重复意味着相同的对象在集合中只会存在一份

集合常见的操作
  • add(value):向集合添加一个新的项;

  • remove(value):从集合中移除一个值;

  • has(value):如果值在集合中,返回true,否则返回false

  • clear():移除集合中的所有项;

  • size():返回集合所包含元素的数量,与数组的length属性相似;

  • values():返回一个包含集合中所有值的数组;

集合间操作:
  • 并集:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合;

  • 交集:对于给定的两个集合,返回一个包含两个集合中共有元素的新集合;

  • 差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合;

  • 子集:验证一个给定集合是否是另一个集合的子集;

注:被箭头函数害惨了..........当方法写成箭头函数,调用会出问题。。。。。

字典的特点
  • 字典存储的是键值对,主要特点是一一对应;

  • 比如保存一个人的信息:数组形式:[19,‘Tom’,1.65],可通过下标值取出信息;字典形式:{"age":19,"name":"Tom","height":165},可以通过key取出value。

  • 此外,在字典中key不能重复无序的,而Value可以重复

字典和映射的关系

  • 有些编程语言中称这种映射关系字典,如Swift中的Dictonary,Python中的dict;

  • 有些编程语言中称这种映射关系Map,比如Java中的HashMap&TreeMap等;

哈希:

目的是用某种方法将字符串转化为数字,作为属性的唯一下标,方便取数据。

优势:

  • 哈希表可以提供非常快速的插入-删除-查找操作

  • 无论多少数据,插入和删除值都只需要非常短的时间,即O(1)的时间级。实际上,只需要几个机器指令即可完成;

  • 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。但是相对于树来说编码要简单得多。

哈希表的一些概念:

  • 哈希化:大数字转化成数组范围内下标的过程,称之为哈希化

  • 哈希函数:我们通常会将单词转化成大数字,把大数字进行哈希化的代码实现放在一个函数中,该函数就称为哈希函数

  • 哈希表:对最终数据插入的数组进行整个结构的封装,得到的就是哈希表

解决冲突常见的两种方案:
  • 方案一:链地址法拉链法);

我们将每一个数字都对10进行取余操作,则余数的范围0~9作为数组的下标值。并且,数组每一个下标值对应的位置存储的不再是一个数字了,而是存储由经过取余操作后得到相同余数的数字组成的数组链表

  • 方案二:开放地址法

开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。

放完冲突项之后如何探测呢

  • 线性探测:到空位置后会停止查找,所以删除模后相同的元素后,不要置为null,可以置为-1

  • 二次探测:解决聚集问题,对步长进行了优化x+1^2、x+2^2、x+3^3

  • 再哈希法(较好!):再做一次哈希,哈希的结果作为步长.再哈希函数公式中的常量利用质数。

为了保证数据在哈希表中均匀分布,当我们需要使用常量的地方,尽量使用质数;比如:哈希表的长度、N次幂的底数等。

性能高的哈希函数应具备以下两个优点:

  • 快速的计算

  • 均匀的分布

哈希函数的实现:
//设计哈希函数
        //1.将字符串转成比较大的数字:hashCode
        //2.将大的数字hasCode压缩到数组范围(大小)之内
        function hashFunc(str,size){
            let hashCode=0
            for(i=0;i<str.length;i++){
                hashCode = 37 * hashCode + str.charCodeAt(i)//多项式算法,质数一般取37
            }
            //取余
            let index=hashCode%size
            return index
        }
        console.log(hashFunc('rrh',7))
        console.log(hashFunc('sd',7))
        console.log(hashFunc('jkh',7))
        console.log(hashFunc('yute',7))
        console.log(hashFunc('rter',7))
        //输出的索引值:4 1 0 5 5...还蛮均匀的
哈希表的常见操作:
  • put(key,value):插入或修改操作;

  • get(key):获取哈希表中特定位置的元素;

  • remove(key):删除哈希表中特定位置的元素;

  • isEmpty():如果哈希表中不包含任何元素,返回trun,如果哈希表长度大于0则返回false;

  • size():返回哈希表包含的元素个数;

  • resize(value):对哈希表进行扩容操作;

如下以put方法为例:

function hashTable(){
            this.storage=[]//数组
            this.count=0//已经存储的元素个数
            //装填因子:loadFactor > 0.75时需要扩容;loadFactor < 0.25时需要减少容量

            this.limit=7//初始长度

            hashTable.prototype.hashFunc=function(str,size){
            let hashCode=0
            for(i=0;i<str.length;i++){
                hashCode = 37 * hashCode + str.charCodeAt(i)//多项式算法,质数一般取37
            }
            //取余
            let index=hashCode%size
            return index
            }

            //put
            hashTable.prototype.put=function(key,value){

                let index=this.hashFunc(key,this.limit)
                let bucket=this.storage[index]//桶数组,给桶编号,注意这里是乱序
                if(bucket==null){//此位置未有过桶
                    bucket=[]//创建一个空桶
                    this.storage[index]=bucket//放一个空桶在索引处
                }
                for(let i=0;i<bucket.length;i++){
                    let tuple=bucket[i]
                    if(tuple[0]==key){
                        tuple[1]=value
                    }
                }
                bucket.push([key,value])
                this.count+=1
                
            }
}
扩容与压缩

为什么需要扩容?

  • 前面我们在哈希表中使用的是长度为7的数组,由于使用的是链地址法,装填因子(loadFactor)可以大于1,所以这个哈希表可以无限制地插入新数据。

  • 但是,随着数据量的增多,storage中每一个index对应的bucket数组(链表)就会越来越长,这就会造成哈希表效率的降低

什么情况下需要扩容?

  • 常见的情况是loadFactor > 0.75的时候进行扩容;

如何进行扩容?

  • 简单的扩容可以直接扩大两倍(关于质数,之后讨论);

  • 扩容之后所有的数据项都要进行同步修改

算法题:判断一个数是否是质数
function isPrime(num){
      if (num <= 1) {
        return false
      }
      //1.获取num的平方根:Math.sqrt(num)
      //2.循环判断
      for(var i = 2; i<= Math.sqrt(num); i++ ){
        if(num % i == 0){
          return false;
        }
      }
        return true;
    }
树:

树结构对比于数组/链表/哈希表有哪些优势呢:

数组:

优点:可以通过下标值访问,效率高;

缺点:查找数据时需要先对数据进行排序,生成有序数组,才能提高查找效率;并且在插入和删除元素时,需要大量的位移操作;

链表:

优点:数据的插入和删除操作效率都很高;

缺点:查找效率低,需要从头开始依次查找,直到找到目标数据为止;当需要在链表中间位置插入或删除数据时,插入或删除的效率都不高。

哈希表:

优点:哈希表的插入/查询/删除效率都非常高;

缺点:空间利用率不高,底层使用的数组中很多单元没有被利用;并且哈希表中的元素是无序的,不能按照固定顺序遍历哈希表中的元素;而且不能快速找出哈希表中最大值或最小值这些特殊值。

树结构优势:

优点:树结构综合了上述三种结构的优点,同时也弥补了它们存在的缺点(虽然效率不一定都比它们高),比如树结构中数据都是有序的,查找效率高;空间利用率高;并且可以快速获取最大值和最小值等。

总的来说:每种数据结构都有自己特定的应用场景

树结构基本用语:

节点的度(Degree):节点的子树个数。一个节点的子树是互不交叉的树.....水平

树的度:所有节点中的度的最大值

叶节点(Leaf):度为0的节点,不再有分支

节点的层次(Level):规定根节点在1层,其他任一节点的层数是其父节点的层数加1

树的深度(Depth):树种节点的最大层次是这棵树的深度

二叉树:

属的两种表示方法:

1.普通表示;2.二叉树

对于二叉树来说的一些公式:

  • 一个二叉树的第 i 层的最大节点树为:2^(i-1),i >= 1;

  • 深度为k的二叉树的最大节点总数为:2^k - 1 ,k >= 1;【等比数列前n项和】

  • 对任何非空二叉树,若 n0 表示叶子节点的个数,n2表示度为2的非叶子节点个数,那么两者满足关系:n0 = n2 + 1

完美二叉树/满二叉树:除了最下一层的叶子节点外,每层节点都有2个子节点。

完全二叉树

  1. 除了二叉树最后一层外,其他各层的节点数都达到了最大值

  1. 最后一层的叶子节点从左向右是连续存在,若缺失,只能缺失右侧若干叶子节点采称为完全二叉树

二叉搜索树(BST,Binary Search Tree):【二叉树最常见的存储方式为链表】

  1. 非空左子树的所有键值小于其根节点的键值

  1. 非空右子树的所有键值大于其根节点的键值

  1. 左、右子树本身也都是二叉搜索树;

总的来说就是小的数靠左,大的数靠右,目的是搜索效率高。

二叉搜索树的常见操作:

insert(key):向树中插入一个新的键;

search(key):在树中查找一个键,如果节点存在,则返回true;如果不存在,则返回false;

inOrderTraverse:通过中序遍历方式遍历所有节点;

preOrderTraverse:通过先序遍历方式遍历所有节点;

postOrderTraverse:通过后序遍历方式遍历所有节点;

min:返回树中最小的值/键;

max:返回树中最大的值/键;

remove(key):从树中移除某个键;

注:用递归很方便完成

insert:

function BinarySearchTree(){
            
            this.root=null

            function Node(key){//用于生成新节点
                this.key=key
                this.left=null
                this.right=null
            }

            //insert
            BinarySearchTree.prototype.insert=function(key){
                let newNode=new Node(key)
                if(this.root==null){this.root=newNode}

                else{//进行递归
                    this.insertNode(this.root, newNode)
                }
            }
            BinarySearchTree.prototype.insertNode=function(node,newNode){//目的是直到找到空节点放进去
                if(newNode.key>node.key){//右侧插入
                    if(node.right==null){
                        node.right=newNode
                    }else{
                        this.insertNode(node.right,newNode)//重新判断比较
                    }
                }else{//左侧插入
                    if(node.left==null){
                        node.left=newNode
                    }else{
                        this.insertNode(node.left,newNode)//重新判断比较
                    }
                }
            }
        }

三种遍历方法实现的重点是,函数执行上下文调用栈的顺序。

先序遍历:(先遍历根节点)

中序遍历:(根节点在中间遍历)

后序遍历:(根节点最后遍历)

以前序遍历为例:

//先序遍历
            BinarySearchTree.prototype.preOrderTraversal=function(fun){//通过传递的结果函数来保存处理结果
                this.preOrderTraversalNode(this.root,fun)
            }
            BinarySearchTree.prototype.preOrderTraversalNode=function(node,handler){
                if(node!=null){//只要节点不为空,就继续遍历

                    handler(node.key)//处理当前节点|第一次进来时处理的是根节点

                    this.preOrderTraversalNode(node.left,handler)//处理左子节点

                    this.preOrderTraversalNode(node.right,handler)//处理右子节点
                }
            }

remove:

  1. 寻找要删除的节点

  1. 找到后节点的3种情况

a.没有子节点:叶子节点

b.只有一个子节点

c.有两个子节点

对于c情况:分别以删除9,7,15为例进行代码编写

删除后需要替换的节点规律是:current左子树中的最大值(前驱);current右子树中的最小值(后继)。

下面只讨论查找current后继的情况,查找前驱的原理相同,这里暂不讨论。

  //删除有两个孩子的叶子节点
                else {
                    let successor = this.getSuccessor(current)//通过当前节点找到后继节点
                    if (current == this.root) {//对当前节点进行判断
                        this.root = successor
                    }else if (isLeftChild){
                        parent.left = successor
                    }else{
                        parent.right = successor
                    }

                    successor.left = current.left//此时的后继节点已经替换完毕,将前驱指针重新设置

                }

                //封装查找后继的方法
                BinarySearchTree.prototype.getSuccessor = function(delNode){
                    //1.定义变量,保存找到的后继
                    let successor = delNode
                    let current = delNode.right
                    let successorParent = delNode

                    //2.循环查找current的右子树节点
                    while(current != null){
                        successorParent = successor
                        successor = current
                        current = current.left
                    }

                    //3.判断寻找到的后继节点是否直接就是删除节点的right节点
                    if(successor != delNode.right){
                        successorParent.left = successor.right//后继节点的子节点移位
                        successor.right = delNode.right //后继节点自身移位
                    }
                    return successor
                }
二叉搜索树的缺陷:

当插入的数据是有序的数据,就会造成二叉搜索树的深度过大。

  • 插入连续数据后,二叉搜索树中的数据分布就变得不均匀了,我们称这种树为非平衡树

  • 对于一棵平衡二叉树来说,插入/查找等操作的效率是O(logN)

  • 而对于一棵非平衡二叉树来说,相当于编写了一个链表,查找效率变成了O(N);

常见的平衡树:
  • AVL树:是最早的一种平衡树,它通过在每个节点多存储一个额外的数据来保持树的平衡。由于AVL树是平衡树,所以它的时间复杂度也是O(logN)。但是它的整体效率不如红黑树,开发中比较少用。

  • 红黑树:同样通过一些特性来保持树的平衡,时间复杂度也是O(logN)。进行插入/删除等操作时,性能优于AVL树,所以平衡树的应用基本都是红黑树。

红黑树:

红黑树除了符合二叉搜索树的基本规则外,还添加了以下特性:

1:节点只有红黑两种;根节点为黑;

2:每个叶子节点都有两个黑空节点(NIL节点);

3:每个红色节点的两个子节点都是黑色的(不可能有两个连续的红色节点);

4:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点;

依据红黑树的规则,就可以确定红黑树的特性:

  • 根到叶子节点最长路径,不会超过最短路径两倍

  • 结果就是这棵树基本是平衡的;虽然没有做到绝对的平衡,但是可以保证在最坏的情况下,该树依然是高效的;

红黑树的变换:

变色、左旋(逆时针)、右旋(顺时针)

红黑树的插入操作:.......好几种情况,有机会再细细分析............

图结构:
  • 主要的研究目的为:事物之间的联系顶点代表事物代表两个事物间的关系

  • 一组顶点:通常用 V (Vertex)表示顶点的集合;

  • 一组边:通常用 E (Edge)表示边的集合;

  • 边是顶点和顶点之间的连线;

  • 边可以是有向的,也可以是无向的。比如A----B表示无向,A ---> B 表示有向;

图结构的表示方法:

邻接矩阵:使用一个二维数组来表示顶点之间的连接

缺点:如果图是一个稀疏图,那么邻接矩阵中将存在大量的 0,造成存储空间的浪费

邻接表:邻接表由图中每个顶点以及和顶点相邻的顶点列表组成。列表:数组/链表/字典(哈希表)

缺点:方便计算某一顶点指向其他顶点的个数,但难于计算某一顶点与其他顶点相连的个数

图结构的遍历:

首先用数组保存顶点,用字典保存边,封装图结构

function Graph (){
      this.vertexes = []  //顶点
      this.edges = new Dictionary() //边

//一.添加顶点
      Graph.prototype.addVertex = function(v){
        this.vertexes.push(v)
        this.edges.set(v, []) //将边添加到字典中,顶点作为键,对应的值为一个存储边的空数组
      }
//二.添加边
      Graph.prototype.addEdge = function(v1, v2){//传入两个相关联的顶点
        this.edges.get(v1).push(v2)//以一个顶点为key取出的数组,并添加
        this.edges.get(v2).push(v1)//是无向表的情况下,要互相指向
      }

//三.实现toString方法:转换为邻接表形式
      Graph.prototype.toString = function (){
        //1.定义字符串,保存最终结果
        let resultString = ""

        //2.遍历所有的顶点以及顶点对应的边
        for (let i = 0; i < this.vertexes.length; i++) {//遍历所有顶点
          resultString += this.vertexes[i] + '-->'
          let vEdges = this.edges.get(this.vertexes[i])
          for (let j = 0; j < vEdges.length; j++) {//遍历字典中每个顶点对应的数组
            resultString += vEdges[j] + '  ';
          }
          resultString += '\n'
        }
        return resultString
      }
 }
  • 广度优先搜索(Breadth - First Search,简称BFS);

实现思路:下为指定的第一个顶点为A时的遍历过程:

  • 如 a 图所示,将在字典edges中取出的与A相邻的且未被访问过的白色顶点B、C、D放入队列que中并变为灰色,随后将A变为黑色并移出队列;

  • 接着,如图 b 所示,将在字典edges中取出的与B相邻的且未被访问过的白色顶点E、F放入队列que中并变为灰色,随后将B变为黑色并移出队列;

  • 如 c 图所示,将在字典edges中取出的与C相邻的且未被访问过的白色顶点G(A,D也相邻不过已变为灰色,所以不加入队列)放入队列que中并变为灰色,随后将C变为黑色并移出队列;

  • 接着,如图 d 所示,将在字典edges中取出的与D相邻的且未被访问过的白色顶点H放入队列que中并变为灰色,随后将D变为黑色并移出队列。

如此循环直到队列中元素为0,即所有顶点都变黑并移出队列后才停止,此时图中顶点已被全部遍历。

//实现广度搜索(BFS)
      //传入指定的第一个顶点和处理结果的函数
      Graph.prototype.bfs = function(initV, handler){
        //1.初始化颜色为white
        let colors = this.initializeColor()
        //2.创建队列
        let que = new Queue()
        //3.将顶点加入到队列中
        que.enqueue(initV)

        //4.循环从队列中取出元素,直到队列为空
        while(!que.isEmpty()){
          //4.1.从队列首部取出一个顶点
          let v = que.dequeue()
          //4.2.从字典对象edges中获取和该顶点相邻的其他顶点组成的数组
          let vNeighbours = this.edges.get(v)
                  //4.3.将v的颜色变为灰色
                  //colors[v] = 'gray'我倒是觉得这一步不用写。。。。

          //4.4.遍历v所有相邻的顶点vNeighbours,并且加入队列中
          for (let i = 0; i < vNeighbours.length; i++) {
            const a = vNeighbours[i];
            //判断相邻顶点是否被探测过,被探测过则不加入队列中;并且加入队列后变为灰色,表示被探测过
            if (colors[a] == 'white') {
              colors[a] = 'gray'
              que.enqueue(a)
            }
          }

          //4.5.处理顶点v
          handler(v)

          //4.6.顶点v所有白色的相邻顶点都加入队列后,将顶点v设置为黑色。此时黑色顶点v位于队列最前面,进入下一次while循环时会被取出
          colors[v] = 'black'
        }
      }
  • 深度优先搜索(Depth - First Search,简称DFS

深度优先搜索算法的遍历顺序与二叉搜索树中的先序遍历较为相似,同样可以使用递归来实现(递归的本质就是函数栈的调用)。

//实现深度搜索(DFS)
      Graph.prototype.dfs = function(initV, handler){
        //1.初始化顶点颜色
        let colors = this.initializeColor()

        //2.从某个顶点开始依次递归访问
        this.dfsVisit(initV, colors, handler)
      }

      //为了方便递归调用,封装访问顶点的函数,传入三个参数分别表示:指定的第一个顶点、颜色、处理函数
      Graph.prototype.dfsVisit = function(v, colors, handler){
        //1.将颜色设置为灰色
        colors[v] = 'gray'

        //2.处理v顶点
        handler(v)

        //3.访问V的相邻顶点
        let vNeighbours = this.edges.get(v)
        for (let i = 0; i < vNeighbours.length; i++) {
          let a = vNeighbours[i];
          //判断相邻顶点是否为白色,若为白色,递归调用函数继续访问
          if (colors[a] == 'white') {
            this.dfsVisit(a, colors, handler)
          }
          
        }

        //4.将v设置为黑色
        colors[v] = 'black'
      }

关于递归:符合条件就能一直执行,直到不符合条件就可以依次跳出函数调用栈回到上一个正在执行的函数,将结果进行处理和返回。当然此处的每次递归调用前先处理结果。

算法:
  • 简单排序:冒泡排序、选择排序、插入排序;

  • 高级排序:希尔排序、快速排序;

常见的大O表示形式:

符号

名称

O(1)

常数

O(log(n))

对数

O(n)

线性

O(nlog(n))

线性和对数乘积

O(n²)

平方

O(2^n

指数

可以看到效率从大到小分别是:O(1)> O(logn)> O(n)> O(nlog(n))> O(n²)> O(2^n

冒泡排序:

两两比较,一趟完成后将最大的放到最后了,第二轮循环再从头开始比较,比较至倒数第二个元素。

function ArrayList() {
        this.array = []

        ArrayList.prototype.insert = function(item){
        this.array.push(item)
        }

        ArrayList.prototype.toString = function(){
            return this.array.join('-')
        }

//交换两个位置的数据
        ArrayList.prototype.swap = function(m, n){
            let temp  = this.array[m]
            this.array[m] = this.array[n]
            this.array[n] = temp
        }

//冒泡排序
        ArrayList.prototype.bubble=function(){
            let length=this.array.length

            for(let j=length-1;j>0;j--){
                for(let i=0;i<length-1;i++){
                //这里一定要写i<length-1而不是i<length,这样才能保证后两位有做比较
                    if(this.array[i]>this.array[i+1]){
                        this.swap(i,i+1)
                    }
                }
            }
        }
    }

冒泡排序的效率【时间复杂度:O(N^2)】:

  • 上面所讲的对于7个数据项,比较次数为:6 + 5 + 4 + 3 + 2 + 1;

  • 对于N个数据项,比较次数为:(N - 1) + (N - 2) + (N - 3) + ... + 1 = N * (N - 1) / 2;如果两次比较交换一次,那么交换次数为:N * (N - 1) / 4;

  • 使用大O表示法表示比较次数和交换次数分别为:O( N * (N - 1) / 2)和O( N * (N - 1) / 4),根据大O表示法的三条规则都化简为:O(N^2);

选择排序:

先取出第一个元素记为最小值,拿这个元素跟数组的其他元素依次比较,如果其他位置的元素比这个元素小,则记下其他位置的下标,一轮循环后拿到了最小元素的下标,再交换位置。第二轮循环从第二个元素开始...

//选择排序
        ArrayList.prototype.select=function(){
            let length=this.array.length
            
            
            for(let j=0;j<length-1;j++){//外层循环,从0开始获取元素;最后一次就不用比较了
                let min=j
                for(let i=min+1;i<length;i++){
                    if(this.array[min]>this.array[i]){
                        min=i//标记最小元素的下标标记为新下标
                    }
                }
                this.swap(j,min)//循环完再交换
            }
        }
    }
  • 选择排序的比较次数为:N * (N - 1) / 2,用大O表示法表示为:O(N^2);

  • 选择排序的交换次数为:(N - 1) / 2,用大O表示法表示为:O(N);

  • 所以选择排序的效率高于冒泡排序;

插入排序:

插入排序思想的核心是局部有序,将第一个元素认为是有序的,拿第二个元素与有序部分依次比较,放入合适位置。依次再拿第三、四、五...等依次与有序部分进行比较。

//插入排序
        ArrayList.prototype.insertion=function(){
            let length=this.array.length

            for(let i=1;i<length;i++){//外层循环确定从1开始获取元素

                let item=this.array[i]//记录当前元素值

                let j=i//记录当前下标,目的是拿下标进行循环比较

                while(this.array[j-1]>item&&j>0){
                    this.array[j]=this.array[j-1]//较大的元素右移
                    j--
                }
                this.array[j] = item//此时经历过循环的j就是合适位置,将当前元素放入即可
            }
        }
  • 比较次数:第一趟时,需要的最大次数为1;第二次最大为2;以此类推,最后一趟最大为N-1;所以,插入排序的最大总比较次数为N * (N - 1) / 2;但是,实际上每趟发现插入点之前,平均只有全体数据项的一半需要进行比较,所以比较次数为:N * (N - 1) / 4

  • 移动次数:指定第一个数据为X时移动0次,指定第二个数据为X最多需要移动1次,以此类推,指定第N个数据为X时最多需要移动N - 1次,所以一共需要交换N * (N - 1) / 2次

  • 虽然用大O表示法表示插入排序的效率也是O(N^2),但是插入排序整体操作次数更少,因此,在简单排序中,插入排序效率最高????是因为移动效率比交换效率高?

希尔排序:

希尔排序首先是对数据分组,每间隔一定增量为一组,组内按照插入排序思想进行排序,每不同的增量内为一次循环,直到增量为1,就近的两个元素排序,即完成。

//希尔排序
        ArrayList.prototype.shell=function(){
            let length=this.array.length
            let gap=Math.floor(length/2) //向下舍入最接近的整数

            while(gap>=1){
                for(let i=gap;i<length;i++){

                    let item=this.array[i]//记录当前元素值
                    let j=i

                    while(this.array[j-gap]>item){
                        this.array[j] = this.array[j - gap]
                        j -= gap
                    }
                    this.array[j] = item
                }

                gap=Math.floor(gap/2)
            }
        }

希尔排序的效率【<O(N^2)】:

  • 希尔排序的效率和增量有直接关系,即使使用原稿中的增量效率都高于简单排序。

快速排序:

快速排序的核心思想是分而治之,先选出一个数据(比如65),将比其小的数据都放在它的左边,将比它大的数据都放在它的右边。这个数据称为枢纽。再分别从左右两边选择枢纽,重复上述操作。其中左右分治需要用到递归。

第一步先选择枢纽:此处是用中位数选取出枢纽,再将其放到倒数第二位

//1.选择枢纽
let median = function(arr){
  //1.取出中间的位置
  let center = Math.floor(arr.length / 2)
  let right = arr.length - 1 
  let left = 0

  //2.判断大小并进行交换
  if (arr[left] > arr[center]) {
    swap(arr, left, center)
  }
  if (arr[center] > arr[right]){
    swap(arr, center, right)
  }
  if (arr[left] > arr[right]) {
    swap(arr, left, right)
  }
  //3.返回枢纽
  return center
}
//2.快速排序
let QuickSort = function(arr){

  if (arr.length == 0) { return []}//直到数组长度为0就结束递连续归地调用

  let center = median(arr)//取出枢纽元素下标
  let c = arr.splice(center, 1)//取出枢纽元素

  let l = []
  let r = []

  for (let i = 0; i < arr.length; i++) {
      if (arr[i] < c) {
        l.push(arr[i])
      }else{
        r.push(arr[i])
      }        
  }
  return QuickSort(l).concat(c, QuickSort(r))
}

快速排序的效率:

  • 快速排序最坏情况下的效率:每次选择的枢纽都是最左边或最右边的数据,此时效率等同于冒泡排序,时间复杂度为O(n2。可根据不同的枢纽选择避免这一情况;

  • 快速排序的平均效率:为O(N*logN),虽然其他算法效率也可达到O(N*logN),但是其中快速排序是最好的

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值