《漫画算法——小灰的算法之旅》读后笔记

看完了《漫画算法》第一遍,以为有些意犹未尽,所以第二次重温时,打算开始作一些笔记,这样能够加深本身对算法知识的理解,也能对没有看过这本书的朋友提供一些帮助。可是具体有几篇笔记,这个须要一边看,一边写,尽可能在双十一以前完成本次观后感的总结吧!html

1、基础概念

算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法表明着用系统的方法描述解决问题的策略机制。也就是说,可以对必定规范的输入,在有限时间内得到所要求的输出。若是一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不一样的算法可能用不一样的时间、空间或效率来完成一样的任务。一个算法的优劣能够用空间复杂度时间复杂度来衡量。java

以上来自百度百科node

算法除了上述具体概念外,对于程序员而言还有不少应用场景,如运算、查找、排序、最优决策等,可是对于大多数像我这样的程序员而言最重要的仍是面试,由于工做中对算法运用的很少,学习这本书主要是增长本身的内功和面试的时候不至于太心慌!git

1.时间复杂度

具体原则有三个:程序员

  • 若是运行时间是常数量级,则用常1表示
  • 只保留函数的最高阶项
  • 若是最高阶项存在,则省去最高阶项前面的系数

如下为具体示例:github

执行次数是线性的 T(n) = 3n面试

fun eat1(n:Int){
    for(i in 0 until n){
        println("等待第一分钟")
        println("等待第二分钟")
        println("第三分钟 吃1cm面包")
    }
}
最高阶项为3n,则省去系数3,时间复杂度表示为:
T(n) = O(n)
复制代码

执行次数是用对数计算的 T(n) = 5logn算法

fun eat2(n:Int){
    for(i in 0 until n){
        println("等待第一分钟")
        println("等待第二分钟")
        println("等待第三分钟")
        println("等待第四分钟")
        println("第五分钟 吃剩下的一半面包")
    }
}
最高阶项为5logn,则省去系数5,时间复杂度表示为:
T(n) = O(logn)
复制代码

执行次数是常量 T(n) = 2数据库

fun eat3(n:Int){
    for(i in 0 until n){
        println("等待第一分钟")
        println("第二分钟 吃1个鸡腿")
    }
}
只有常数量级,则时间复杂度表示为:
T(n) = O(1)
复制代码

执行次数是多项式计算的 `T(n) = 0.5n*n+0.5n数组

fun eat4(n:Int){
    for (i in 0 until n){
            for (j in 0 until i){
                println("等待1分钟")
            }
            println("吃1cm面包")
        }
}
最高阶项是0.5n*n,则时间复杂度表示为:
T(n) = O(n*n)
复制代码

结果比较后得出结论以下:

O(1) < O(logn) < O(n) < O(n*n)

2.空间复杂度

空间复杂度和上面的时间复杂度相似,有如下四种类型:

  • 常量类型 算法存储空间与输入规模大小没有直接关系,记做O(1)
fun fun1(n:Int){
    var value = 3
    ...
}
复制代码
  • 线性空间 当算法分配的空间是一个线性的集合(如数组),且集合的大小和输入的规模成正比,记做O(n)
fun fun2(n:Int){
    var array = IntArray(n)
    ...
}
复制代码
  • 二维空间 当算法分配的空间是一个二维数组集合,且集合的长度和宽度与输入的规模成正比,记做O(n*n)
void fun3(int n){
    int[][] matrix = new int[6][6];
    ...
}
复制代码
  • 递归空间 执行递归所须要的内存空间和递归的深度成正比,记做O(n)
fun fun4(n:Int){
    if(n<=1){
        return
    }
    fun4(n-1)
}
复制代码

3.数据结构 data structure

  • 线性结构

    • 数组

    数组 array,是有限个相同类型的变量所组成的有序集合,数组中的每个变量被称之为元素。优势,由于其在内存在顺序存储,所以拥有很是高效的随机访问能力,只要经过下标就能够直接获取元素,缺点是插入会致使大量数据被迫移动,影响效率(例如HashMap扩容)。适合读操做多,写操做少的业务场景。

    • 链表

    链表是一种数据结构,分为单链表和双链表。下面这张图就是单链表

    若是以为仍是抽象的话,那咱们一块儿看看代码吧!

/**
 * <p>文件描述:链表的基本操做<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/10/17 0017 <p>
 * <p>@update 2019/10/17 0017<p>
 * <p>版本号:1<p>
 *
 */
// 单链表
data class Node(var data:Int,var next: Node? = null)
// 双链表
data class DoubleNode(var data: Int, var prev: DoubleNode?, var next: DoubleNode?)

class Test {
    // 头节点指针
    private  var head: Node? = null
    // 尾节点指针
    private  var last: Node? = null
    // 链表实际长度
    private var size = 0

    // 插入链表元素
    fun insert(data:Int,index:Int){
        if(index<0 || index>size){
            throw IndexOutOfBoundsException("超出链表节点范围")
        }
        val insertNode = Node(data)
        if(size == 0){
            // 空链表
            head = insertNode
            last = insertNode
        }else if(index == 0){
            // 插入头部
            insertNode.next = head
            head = insertNode
        }else if (size == index){
            // 插入尾部
            last?.next = insertNode
            last = insertNode
        }else{
            // 插入中间
            val prevNode = getNode(index-1)
            insertNode.next = prevNode?.next
            prevNode?.next = insertNode
        }
        size++
    }

    fun remove(index: Int): Node?{
        if(index<0 || index>=size){
            throw IndexOutOfBoundsException("超出链表节点范围")
        }
        var removeNode: Node? = null
        if(index == 0){
            // 删除头节点
            removeNode = head
            head = head?.next
        }else if (index == size-1){
            // 删除尾节点
            removeNode = getNode(index-1)
            removeNode?.next = null
            last = removeNode

        }else {
            // 删除中间节点
            val prevNode = getNode(index-1)
            removeNode = prevNode?.next
            val nextNode = prevNode?.next?.next
            prevNode?.next = nextNode
        }
        size--
        return removeNode
    }
    fun outPut(){
        var  temp = head
        while (temp != null){
            print(temp.data)
            temp = temp.next
        }
    }

    /**
     * 链表查找元素
     */
    private fun getNode(index: Int): Node? {
        if(index<0 || index>=size){
            throw IndexOutOfBoundsException("超出链表节点范围")
        }
        var temp: Node? = head
        for (i in 0 until index){
            temp = temp?.next
        }
        return temp
    }


}
复制代码

测试一下上面的这个单链表:

/**
 * <p>文件描述:数据结构测试<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/10/17 0017 <p>
 * <p>@update 2019/10/17 0017<p>
 * <p>版本号:1<p>
 *
 */
class DataStructure {
    @org.junit.Test
    fun testLinkedList() {
        val test = Test()
        test.insert(3,0)
        test.outPut()
        println()
        test.insert(7,1)
        test.outPut()
        println()
        test.insert(9,2)
        test.insert(5,3)
        test.outPut()
        println()
        test.insert(4,2)
        test.outPut()
        println()
        test.remove(0)
        test.outPut()
    }
}
复制代码

运行结果:

3
37
379
3795
37495
7495
复制代码

链表和数组相比,数组的优点仍是快速定位元素,适合读操做多,写操做少的业务场景;链表则相对于删除和尾部插入更合适,所以适用于频繁插入删除的业务情景

java.util.Stack是一种先入后出的线性数据结构,如图:

package java.util;

/**
 * The <code>Stack</code> class represents a last-in-first-out
 * (LIFO) stack of objects. It extends class <tt>Vector</tt> with five
 * operations that allow a vector to be treated as a stack. The usual
 * <tt>push</tt> and <tt>pop</tt> operations are provided, as well as a
 * method to <tt>peek</tt> at the top item on the stack, a method to test
 * for whether the stack is <tt>empty</tt>, and a method to <tt>search</tt>
 * the stack for an item and discover how far it is from the top.
 * <p>
 * When a stack is first created, it contains no items.
 *
 * <p>A more complete and consistent set of LIFO stack operations is
 * provided by the {@link Deque} interface and its implementations, which
 * should be used in preference to this class.  For example:
 * <pre>   {@code
 *   Deque<Integer> stack = new ArrayDeque<Integer>();}</pre>
 *
 * @author  Jonathan Payne
 * @since   JDK1.0
 */
public class Stack<E> extends Vector<E> {
    /**
     * Creates an empty Stack.
     */
    public Stack() {
    }

    /**
     * Pushes an item onto the top of this stack. This has exactly
     * the same effect as:
     * <blockquote><pre>
     * addElement(item)</pre></blockquote>
     *
     * @param   item   the item to be pushed onto this stack.
     * @return  the <code>item</code> argument.
     * @see     java.util.Vector#addElement
     */
    public E push(E item) {
        addElement(item);

        return item;
    }

    /**
     * Removes the object at the top of this stack and returns that
     * object as the value of this function.
     *
     * @return  The object at the top of this stack (the last item
     *          of the <tt>Vector</tt> object).
     * @throws  EmptyStackException  if this stack is empty.
     */
    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    /**
     * Looks at the object at the top of this stack without removing it
     * from the stack.
     *
     * @return  the object at the top of this stack (the last item
     *          of the <tt>Vector</tt> object).
     * @throws  EmptyStackException  if this stack is empty.
     */
    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }

    /**
     * Tests if this stack is empty.
     *
     * @return  <code>true</code> if and only if this stack contains
     *          no items; <code>false</code> otherwise.
     */
    public boolean empty() {
        return size() == 0;
    }

    /**
     * Returns the 1-based position where an object is on this stack.
     * If the object <tt>o</tt> occurs as an item in this stack, this
     * method returns the distance from the top of the stack of the
     * occurrence nearest the top of the stack; the topmost item on the
     * stack is considered to be at distance <tt>1</tt>. The <tt>equals</tt>
     * method is used to compare <tt>o</tt> to the
     * items in this stack.
     *
     * @param   o   the desired object.
     * @return  the 1-based position from the top of the stack where
     *          the object is located; the return value <code>-1</code>
     *          indicates that the object is not on the stack.
     */
    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = 1224463164541339165L;
}
复制代码
  • 队列

队列和栈极其类似,两者的区别点在于栈是先进后出(FILO),而队列是先入先出(FIFO

有意思的是咱们能够经过数组来实现这个数据操做,且避免了总体数据迁移的麻烦!

package com.vincent.algorithmapplication.data_structure

import java.lang.Exception

/**
 * <p>文件描述:自定义对列的实现<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/10/23 0023 <p>
 * <p>@update 2019/10/23 0023<p>
 * <p>版本号:1<p>
 *
 */
class MyQueue (capaicty:Int){
    private  var array:IntArray
    var front = 0
    var rear = 0
    init {
        array = IntArray(capaicty)
    }

    fun enQueue(element:Int){
        if((rear+1)%array.size == front){
            throw Exception("队列已满")
        }
        array[rear] = element
        rear = (rear+1)%array.size
    }

    fun deQueue():Int{
        if(rear == front){
            throw Exception("队列已空")
        }
        val deQueueElement = array[front]
        front = (front+1)%array.size
        return deQueueElement
    }

    fun output(){
        for (i in array){
            print(i)
        }
        println()
    }
}

//测试:
    fun testMyQueue(){
        val myQueue = MyQueue(6)
        myQueue.enQueue(3)
        myQueue.enQueue(6)
        myQueue.enQueue(2)
        myQueue.enQueue(7)
        myQueue.enQueue(5)
        myQueue.deQueue()
        myQueue.enQueue(9)
        myQueue.deQueue()
        myQueue.enQueue(4)
        myQueue.deQueue()
        myQueue.enQueue(1)
        myQueue.output()
    }

复制代码

测试结果:

412759

复制代码

栈的应用业务对于历史操做的记录,好比返回上一步,而队列用于历史记录顺序的保存,好比爬虫脚本抓取的地址顺序存放,后面操做的时候顺序读取。

扩展:

双端队列——结合了栈和队列的特色,可从对头或者队尾插入或者删除

优先队列——根据元素的优先级决定谁最早出队

  • 哈希表

哈希表也叫作散列表,这种数据结构提供键(key)和值(value)的映射关系来实现数据存取。 主要三个知识点:

  • 哈希函数——将键值转化为值的数组下标(不是说值就是数组储存,直接抽象这么理解思路便可)
  • 写操做(哈希冲突)与读操做 写操做的时候不免遇到两个不一样的键值转化结果是同一个下标值,此时有两种处理方式,一种是安卓中的ThreadLocal使用的开放寻址法,一种是HashMap中使用的链表法。读操做就简单了,根据写操做选择对应的方法取值便可
  • 扩容 散列表空间即将达到饱和时须要进行扩容,以HashMap举例:
HashMap.Size 散列表空间
Capacity HashMap 当前已用空间
LoadFactor HashMap 负载因子,默认值0.75
HashMap.Size >= Capacity * LoadFactor
复制代码

扩容时除了须要将原先的空间扩大两倍之外,还要将全部的值从新通过哈希函数定位保存。

看了这部份内容的时候,让我有一种从新去翻看HashMap源码的冲动

  • 树 后面第二部分单独介绍
  • 图 处理相似数据库同样复杂的多对的关系的数据结构,书中没有给出具体案例。
  • 其它
    • 位图 后面的算法当中有用到

除了做者划分的这几个类型,咱们还能够看看百度对数据结构的划分:

2、数和二叉树那些事

1.什么是树(tree

数据结构中的,指的是n(n>=0)个节点的有限集。当n=0时称为空树。在任意一个非空树中,有如下特色:

  • 有且仅有一个特定的称为根的节点。
  • n>1时,其他节点可分为m(m>0)个互不相交的有限集,每个有限集自己又是一个树,并称为根的子树。
    树状图

在上图中,节点1根节点,像节点7这样没有孩子节点的称之为叶子节点,其中的节点24589又构成了节点1子树

节点4的上一级节点是它的父节点,节点4衍生出来的节点8和节点9属于它的孩子节点,也可称为左孩子和右孩子,节点8和节点9因为是同一个父节点,所以他们又互为兄弟节点

树的最大层级数称为树的高度或者深度,图中树的深度明显是4

二叉树主要是一种逻辑思惟,其代码具体实现(也称为物理存储结构)既支持链式存储也支持数组存储,固然咱们通常都是数组存储。

上面的概念不用死记硬背,知道是什么意思便可,主要是方便后续文章表达,避免各位看官老爷一头雾水!

2.什么是二叉树(binart)?

准备好,一堆概念即未来袭!

二叉树是树的一种特殊形式,(这里的二叉并不是骂人),顾名思义,树的每一个节点最多有2个孩子节点。特别强调,这里表达的是最多2个,也包含只有1个节点或者没有子节点的状况。

二叉树还有多种表现形式,好比:

  • 满二叉树(有文章也称为完美二叉树)

二叉树的全部非叶子节点均存在左右孩子,而且全部叶子节点都在同一层级上,那么这个数就是满二叉树!若是硬要用一句话解释:只要你有孩子,你就必然是有两个孩子!若是看不懂也没有关系,我们直接看图!

  • 彻底二叉树

对一个有n个节点的二叉树,按层级序号编写,则全部节点的编号为从1n。若是这个数的全部节点和一样深度的满二叉树的编号从1n的节点位置相同,则这个二叉树为彻底二叉树。若是看不懂,那就对了。我最初也看不懂,直接看图,再回头来看看定义!

树状图
建议和满二叉树的示例图一块儿看更容易理解。

  • 二叉查找树

二叉查找树,也称为二叉排序树(Binary Sort Tree),还有一个名字是二叉搜索树,指的是在二叉树的基础上增长了三个条件:

  • 若是左子树不为空,则左子树上全部的节点的值小于根节点的值
  • 若是右子树不为空,则右子树上全部节点的值均大于根节点的值
  • 左右子树都是二叉查找树

在节点如上图分布相对均衡的时候,二叉查找树的时间复杂度是 O(logn),若是节点分布以下图的话,其时间复杂度将达到 O(n)

  • 红黑树Red Black Tree

红黑树是一种平衡二叉树,除了知足二叉查找树的规定外,还有本身的5点规定:

  • 节点是红色或黑色
  • 根节点是黑色
  • 每一个叶子节点都是黑色的空节点(NIL节点)
  • 每一个红色节点的两个子节点都是黑色。(从每一个叶子到根的全部路径上不能有两个连续的红色节点)
  • 从任一节点到其每一个叶子的全部路径都包含相同数目的黑色节点。
    简单来说,红黑树经过左旋转、右旋转、变色三种方式组合使用来实现自平衡的,具体过程建议查看原文什么是红黑树

3.二叉树的遍历

二叉树因为其自身的复杂性,其遍历也有多种方式,从宏观的角度能够深度优先遍历广度优先遍历,从节点之间的位置关系来分,有如下四种:

  • 前序遍历 (深度优先遍历) 输出顺序:根节点-->左子树-->右子树
  • 中序遍历 (深度优先遍历) 左子树-->跟节点-->右子树
  • 后序遍历 (深度优先遍历) 左子树-->右子树-->跟节点
  • 层序遍历 (广度优先遍历) 根节点-->叶子节点

概念提及来抽象,我们直接见代码:

/**
 * <p>文件描述:二叉树经过递归的方式实现的遍历<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/10/25 0025 <p>
 * <p>@update 2019/10/25 0025<p>
 * <p>版本号:1<p>
 *
 */

// 二叉树实体
data class TreeNode(val data: Int) {
    var leftChild: TreeNode? = null
    var rightChild: TreeNode? = null
}

object TreeSort {
    fun createBinaryTree(inputList: LinkedList<Int?>?): TreeNode? {
        if (inputList == null || inputList.isEmpty()) {
            return null
        }
        var node: TreeNode? = null
        var data = inputList?.removeFirst()
        if (data != null) {
            node = TreeNode(data)
            node.leftChild = createBinaryTree(inputList)
            node.rightChild = createBinaryTree(inputList)
        }
        return node

    }

    // 二叉树前序排列
    fun preOrderTraveral(node: TreeNode?) {
        if (node == null) return
        print(node.data)
        preOrderTraveral(node.leftChild)
        preOrderTraveral(node.rightChild)
    }


    // 二叉树中序排列
    fun inOrderTraveral(node: TreeNode?) {
        if (node == null) return
        inOrderTraveral(node.leftChild)
        print(node.data)
        inOrderTraveral(node.rightChild)
    }


    // 二叉树后序排列
    fun postOrderTraveral(node: TreeNode?) {
        if (node == null) return
        postOrderTraveral(node.leftChild)
        postOrderTraveral(node.rightChild)
        print(node.data)
    }
    
    
     // 二叉树层序遍历
    fun levelOrderTraversal(node: TreeNode?){
        val queue = LinkedList<TreeNode>()
        queue.offer(node)
        while (queue.isEmpty().not()){
            val itemTreeNode = queue.poll()
            print(itemTreeNode.data)
            if(itemTreeNode.leftChild != null){
                queue.offer(itemTreeNode.leftChild)
            }
            if(itemTreeNode.rightChild != null){
                queue.offer(itemTreeNode.rightChild)
            }
        }

    }

    // 二叉树前序排列 栈
    fun preOrderTraveralWithStack(node: TreeNode?) {
        val stack = Stack<TreeNode>()
        var root = node
        while (root != null || stack.isEmpty().not()) {
            while (root != null) {
                print(root.data)
                stack.push(root)
                root = root.leftChild
            }
            while (stack.empty().not()) {
                root = stack.pop()
                root = root.rightChild
                if (root != null) {
                    break
                }
            }
        }
    }
    // 二叉树中序排列 栈
    fun inOrderTraveralWithStack(node: TreeNode?) {
        val stack = Stack<TreeNode>()
        var root = node
        while (root != null || stack.isEmpty().not()) {
            while (root != null) {
                stack.push(root)
                root = root.leftChild
            }
            while (stack.empty().not()) {
                root = stack.pop()
                print(root.data)
                root = root.rightChild
                if (root != null) {
                    break
                }
            }
        }
    }

    // 二叉树后序排列 栈
    fun postOrderTraveralWithStack(node: TreeNode?) {
        val stack = Stack<TreeNode>()
        var root = node
        while (root != null || stack.isEmpty().not()) {
            while (root != null) {
                stack.push(root)
                root = root.leftChild
            }
            while (stack.empty().not()) {
                root = stack.pop()
                if (root?.rightChild != null) {
                    val parent = TreeNode(root.data)
                    stack.push(parent)
                    root = root.rightChild
                    break
                } else {
                    print(root.data)
                    root = null
                }
            }
        }
    }
}
复制代码

测试代码:

fun sortTreeNode() {

    val inputList = LinkedList<Int?>()
    inputList.addAll(arrayListOf(1, 3, 9, null, null, 5, null, null, 7, null, 8))
    val treeNode = TreeSort.createBinaryTree(inputList)
    println("前序排列")
    TreeSort.preOrderTraveral(treeNode)
    println()
    println("前序排列 栈")
    TreeSort.preOrderTraveralWithStack(treeNode)
    println()
    println("中序排列")
    TreeSort.inOrderTraveral(treeNode)
    println()
    println("中序排列 栈")
    TreeSort.inOrderTraveralWithStack(treeNode)
    println()
    println("后续排列")
    TreeSort.postOrderTraveral(treeNode)
    println()
    println("后序排列 栈")
    TreeSort.postOrderTraveralWithStack(treeNode)
    println()
    println("层序遍历")
    TreeSort.levelOrderTraversal(treeNode)
}
复制代码

运行结果:

前序排列
139578
前序排列 栈
139578
中序排列
935178
中序排列 栈
935178
后续排列
953871
后序排列 栈
953871
层序遍历
137958
复制代码

数据内容:

注意:书中的“二叉树非递归前序遍历”代码有个小错误,循环中没有置空treeNode致使有元素丢失。 这部分代码若是思路不清楚,不妨跟着断点,看看代码运行逻辑。

4.什么是二叉堆?

二叉堆其实就是一种特殊的彻底二叉树,它有两个类型:

  • 最大堆

最大堆的任何一个父节点的值,都大于或等于它左右孩子的节点的值

  • 最小堆

最大堆的任何一个父节点的值,都小于或等于它左右孩子的节点的值

观察堆顶元素得知,最大堆的对顶元素是整个堆中的最大元素,反之,最小堆的对顶是整个堆中最小的元素

代码实现:

/**
 * <p>文件描述:二叉堆的实现<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/10/27 0027 <p>
 * <p>@update 2019/10/27 0027<p>
 * <p>版本号:1<p>
 *
 */
object BinaryHeap {
    // “上浮”调整(插入节点排序)
    fun upAdjust(array: IntArray){
        var childIndex = array.size-1
        var parentIndex = (childIndex-1)/2
        var temp = array[childIndex]
        while (childIndex>0 && temp<array[parentIndex]){
            array[childIndex] = array[parentIndex]
            childIndex = parentIndex
            parentIndex = (parentIndex-1)/2
        }
        array[childIndex] = temp
    }

    // “下沉”调整 (删除节点)
    private fun downAdjust(array: IntArray,index:Int,length:Int){
        var parentIndex = index
        var temp = array[parentIndex]
        var childIndex = 2 * parentIndex + 1
        while (childIndex < length){
            if(childIndex+1 < length && array[childIndex+1] < array[childIndex]){
                childIndex++
            }
            if(temp <= array[childIndex]){
                break
            }
            array[parentIndex] = array[childIndex]
            parentIndex = childIndex
            childIndex = 2 * childIndex + 1
        }
        array[parentIndex] = temp
    }

    fun buildHeap(array: IntArray){
        for (i in (array.size-2)/2 downTo 0){
            downAdjust(array,i,array.size)
        }
    }
}
复制代码

测试:

fun testBinaryHeap(){
    var array = intArrayOf(1,2,3,4,5,6,7,8,9,0)
    BinaryHeap.upAdjust(array)
    println(array.contentToString())

    array = intArrayOf(7,1,3,10,5,2,8,9,6)
    BinaryHeap.buildHeap(array)
    println(array.contentToString())

}
复制代码

运行结果:

[0, 1, 3, 4, 2, 6, 7, 8, 9, 5]
[1, 5, 2, 6, 7, 3, 8, 9, 10]
复制代码

5.什么是优先队列?

上面介绍了队列的先进先出(FIFO)特性,而优先队列严格意义上来说并非队列,由于优先队列有本身的出队顺序,并且还有有两种状况:

  • 最大优先队列——当前最大的元素优先出队
  • 最小优先队列——当前最小的元素优先出队

代码实现:

/**
 * <p>文件描述:优先队列的实现<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/10/31 0027 <p>
 * <p>@update 2019/10/31 0027<p>
 * <p>版本号:1<p>
 *
 */
class PriorityQueue {
    // 默认长度
    private var array = IntArray(16)
    private var size = 0

    // 入队
    fun enQueue(value: Int) {
        // 判断是否须要扩容
        if (size >= array.size) {
            reSize()
        }
        array[size++] = value
        upAdjust()
    }

    // 出队
    fun deQueue(): Int {
        if (size == 0) {
            throw Exception("the array is empty")
        }
        // 获取对顶元素
        val first = array.first()
        // 最后一个元素移到堆顶
        array[0] = array[--size]
        downAdjust()
        return first
    }

    // 队列扩容
    private fun reSize() {
        this.array = array.copyOf(2 * size)
    }

    // “上浮”调整
    private fun upAdjust() {
        var childIndex = size - 1
        var parentIndex = (childIndex - 1) / 2
        // 临时保存
        val temp = array[childIndex]
        while (childIndex > 0 && temp > array[parentIndex]) {
            // 赋值
            array[childIndex] = array[parentIndex]
            childIndex = parentIndex
            parentIndex /= 2
        }
        array[childIndex] = temp
    }

    // “下沉”调整
    private fun downAdjust() {
        var parentIndex = 0
        // 保存根节点的值
        val temp = array.first()
        var childIndex = 1
        while (childIndex < size) {
            // 若是有右孩子,且右孩子大于左孩子的值,则定位到右孩子
            if (childIndex + 1 < size && array[childIndex + 1] > array[childIndex]) {
                childIndex++
            }
            // 若是父节点大于子节点的值,跳出循环
            if (temp >= array[childIndex]) {
                break
            }
            // 赋值
            array[parentIndex] = array[childIndex]
            parentIndex = childIndex
            childIndex = 2 * childIndex + 1
        }
        array[parentIndex] = temp
    }
}
复制代码

测试:

fun testPriorityQueue(){
    val priorityQueue = PriorityQueue()
    priorityQueue.enQueue(3)
    priorityQueue.enQueue(5)
    priorityQueue.enQueue(10)
    priorityQueue.enQueue(2)
    priorityQueue.enQueue(7)
    println(priorityQueue.deQueue())
    println(priorityQueue.deQueue())
}
复制代码

运行结果:

10
7
复制代码

3、排序算法

排序算法,我之前只知道二分查找法冒泡排序法,后来才阅读本章内容的时候才发现原来一个简单的排序有10排序逻辑,根据性能排序以下:

排序算法 平均时间复杂度
冒泡排序 O(n2)
选择排序 O(n2)
插入排序 O(n2)
希尔排序 O(n1.5)
快速排序 O(N*logN)
归并排序 O(N*logN)
堆排序 O(N*logN)
基数排序 O(d(n+r))
以上数据摘自由科普中国审核的百度百科词条

1.什么是冒泡排序?

冒泡排序(bubble sort)是一种基础的交换排序,具体作法:把相邻的元素两两比较,当一个元素大于右侧相邻元素时,交换它的位置;当一个元素小于或者等于右侧相邻元素时,位置不变。

如图所示,遍历数组,将每个元素按照效果图执行排序便可获得最终的效果。

代码实现:

object BubbleSort {
    fun sort(array: IntArray){
        for (i in array.indices){
            for (j in 0 until array.size-i-1){
                if(array[j]>array[j+1]){
                    var temp = array[j]
                    array[j] = array[j+1]
                    array[j+1] = temp
                }
            }
        }
    }
}
复制代码

测试代码:

fun sort1(){
    val array = intArrayOf(5,8,6,3,9,4,7,1,2,6)
    BubbleSort.sort(array)
    println(array.contentToString())
}
复制代码

运行结果:

[1, 2, 3, 4, 5, 6, 6, 7, 8, 9]
复制代码

接下来咱们继续优化,首先看看整个运行过程:

数据源——5,8,6,3,9,4,7,1,2,6

第一次——5,6,3,8,4,7,1,2,6,9

第二次——5,3,6,4,7,1,2,6,8,9

第三次——3,5,4,6,1,2,6,7,8,9

第四次——3,4,5,1,2,6,6,7,8,9

第五次——3,4,1,2,5,6,6,7,8,9

第六次——3,1,2,4,5,6,6,7,8,9

第七次——1,2,3,4,5,6,6,7,8,9

第八次——1,2,3,4,5,6,6,7,8,9

第九次——1,2,3,4,5,6,6,7,8,9

第十次——1,2,3,4,5,6,6,7,8,9

经过分析发现,第八次到第十次的结果与第七次排序没有差异,换句话说就是第八到第十次排序是徒劳的。这个地方咱们看看能不能经过一个标志位来解决徒劳的浪费问题? 代码实现:

// 第一轮优化后的冒泡排序
fun sort2(array: IntArray){
    for (i in array.indices){
        // 标志位
        var isSorted = true
        for (j in 0 until array.size-i-1){
            if(array[j] > array[j+1]){
                var tmp = array[j]
                array[j] = array[j+1]
                array[j+1] = tmp
                // 有元素交换,修改标志位
                isSorted = false
            }
        }
        if(isSorted)break
    }
}
复制代码

测试代码与上面类似,且结果是一致的,这里就不贴代码了。上面优化的是外部的大循环,那么里面的小循环能不能优化呢? 咱们先看看书上的示例:34215678

再看看运行的动图:

第二次及之后排序的时候,能不能对 5678忽略?继续看代码实现:

// 第二轮优化 冒泡排序
fun sort3(array: IntArray) {
    // 记录最后一次交换的位置
    var lastExchangeIndex = 0
    // 排序边界
    var sortBorder = array.size - 1
    for (i in array.indices) {
        // 标志位
        var isSorted = true
        for (j in 0 until sortBorder) {
            if (array[j] > array[j + 1]) {
                var tmp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = tmp
                // 有元素交换,修改标志位
                isSorted = false
                // 交换元素的边界
                lastExchangeIndex = j
            }
        }
        sortBorder = lastExchangeIndex
        if (isSorted) break
    }
}
复制代码

测试代码:

fun sort1(){
        val array = intArrayOf(2,4,1,5,3,6,7,8,9)//5,8,6,3,9,4,7,1,2,6
//        BubbleSort.sort(array)
//        BubbleSort.sort2(array)
        BubbleSort.sort3(array)
        println(array.contentToString())

    }
复制代码

运行结果:

[1, 2, 3, 4, 5, 6, 7, 8, 9]
复制代码

一个简单的冒泡排序,咱们写了三种。其实还有一种基于冒泡排序的升级排序方法:鸡尾酒排序

鸡尾酒排序(双向冒泡排序)

冒泡排序是依次从左至右的单向排序,而鸡尾酒排序则是从左至右,而后从右至左的钟摆式双向排序,具体过程见下面gif动图

图片源自 blog.csdn.net/sgn132/arti…

知道具体原理了,接下来咱们再看看代码如何实现?

// 双向排序  鸡尾酒排序
fun sort4(array: IntArray){
    var tmp = 0
    for (i in 0 until array.size/2){
        // 标志位
        var isSorted = true
        for (j in i until array.size-i-1){
            if(array[j] > array[j+1]){
                tmp = array[j]
                array[j] = array[j+1]
                array[j+1] = tmp
                isSorted = false
            }
        }
        if(isSorted)break
        isSorted = true
        for (k in array.size-i-1 downTo i+1){
            if(array[k] < array[k-1]){
                tmp = array[k]
                array[k] = array[k-1]
                array[k-1] = tmp
                isSorted = false
            }
        }
        if(isSorted)break
    }
}
复制代码

代码测试:

fun sort1(){
        val array = intArrayOf(2,4,1,5,3,6,7,8,9)//5,8,6,3,9,4,7,1,2,6
//        BubbleSort.sort(array)
//        BubbleSort.sort2(array)
//        BubbleSort.sort3(array)
        BubbleSort.sort4(array)
        println(array.contentToString())

    }
复制代码

测试结果:

[1, 2, 3, 4, 5, 6, 7, 8, 9]
复制代码

2.什么是快速排序?

快速排序Quicksort)和冒泡排序同样,都是交换排序,在每一轮挑选一个基准元素,并让其它比它大的元素移动到数列的一边,比它小的元素移动到数列的另外一边,从而把数列拆解成两个部分。这个思路称之为分治法

单边循环法

若是以为这个概念比较绕,你能够直接理解为将数组分红一半,而后对一半再分红一半的递归。若是实在不能理解,那咱们来看看代码是怎么解释这件事的:

/**
 * <p>文件描述:快速排序的实现<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/11/01 0027 <p>
 * <p>@update 2019/11/01 0027<p>
 * <p>版本号:1<p>
 *
 */
object QuickSort {
    // 快速排序
    fun sort(array: IntArray, startIndex: Int, endIndex: Int) {
        // 递归结束条件
        if (startIndex >= endIndex) return
        val pivotIndex = partition(array, startIndex, endIndex)
        // 根据基准元素,对分红两部分的数组进行递归排序
        sort(array, startIndex, pivotIndex - 1)
        sort(array, pivotIndex + 1, endIndex)
    }

    // 获取基准元素
    // arr 须要排序的数组
    // startIndex 获取基准元素的起点边界
    // endIndex 获取基准元素的终点边界
    private fun partition(array: IntArray, startIndex: Int, endIndex: Int): Int {
        // 获取基准元素 默认获取第一个,也可获取其它任意元素
        // 根据数据状况选择不一样的基准元素能够提升效率
        val pivot = array[startIndex]
        var mark = startIndex
        for (i in startIndex+1..endIndex){
            if(array[i] < pivot){
                mark++
                val tmp = array[mark]
                array[mark] = array[i]
                array[i] = tmp
            }
        }
        array[startIndex] = array[mark]
        array[mark] = pivot
        return mark
    }
}
复制代码

接下来测试一波,看看效果:

fun quickSort(){
    val array = intArrayOf(4,6,5,2,8,9,7,5,1,3)
    QuickSort.sort(array,0,array.size-1)
    println(array.contentToString())
}
复制代码

测试结果:

[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
复制代码

上面的代码结合示意图应该很好的理解,可是不知道各位童鞋发现没有,这个过程和冒泡排序很相似,都是一轮一轮的不断交换顺序,那么咱们能不能像鸡尾酒同样从两端钟摆式的双向循环呢?答案是确定的!

双边循环法

先看看一个示例图:

知道是怎么一回事了吗?不知道的话,咱们再看看代码怎么实现?

// 快速排序
fun sort(array: IntArray, startIndex: Int, endIndex: Int,isDouble:Boolean) {
    // 递归结束条件
    if (startIndex >= endIndex) return
    val pivotIndex = if(isDouble){
        partitionWithDouble(array, startIndex, endIndex)
    }else{
        partition(array, startIndex, endIndex)
    }

    // 根据基准元素,对分红两部分的数组进行递归排序
    sort(array, startIndex, pivotIndex - 1,isDouble)
    sort(array, pivotIndex + 1, endIndex,isDouble)
}

// 获取双边循环的基准元素
// arr 须要排序的数组
// startIndex 获取基准元素的起点边界
// endIndex 获取基准元素的终点边界
private fun partitionWithDouble(array: IntArray, startIndex: Int, endIndex: Int): Int{
    // 获取基准元素 默认获取第一个,也可获取其它任意元素
    // 根据数据状况选择不一样的基准元素能够提升效率
    val pivot = array[startIndex]
    var left = startIndex
    var right = endIndex
    while (left!=right){
        // 控制right指针比较并左移
        while (left<right && array[right] > pivot){
            right--
        }
        // 控制left指针比较并右移
        while (left<right && array[left] <= pivot){
            left++
        }
        if(left<right){
            val tmp = array[left]
            array[left] = array[right]
            array[right] = tmp
        }

    }
    // pivot和指针重合点交换
    array[startIndex] = array[left]
    array[left] = pivot
    return left
}
复制代码

测试代码:

fun quickSort(){
    var array = intArrayOf(4,6,5,2,8,9,7,5,1,3)
    QuickSort.sort(array,0,array.size-1,false)
    println(array.contentToString())
    array = intArrayOf(4,8,6,7,4,2,1,5,3,9,5)
    QuickSort.sort(array,0,array.size-1,true)
    println(array.contentToString())
}
复制代码

运行结果:

[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9]
复制代码

从代码来看,单边循环法获取元素是经过每轮从左到右的依次比较来获取,而双边循环法是每一轮从左到右,而后再从右到左的循环回来,缩小循环的边界。具体过程能够跟着断点仔细看一看。 上面的快速排序都是经过递归来实现的,那么能不能经过其它方式实现呢?这个答案也是确定的,这里也经过来实现!

非递归实现

非递归排序说来很简单,就是把每次递归的方法本身用栈来封装,而非每次经过调用自身的递归方式来实现。概念太抽象了,咱们看看代码的实现?

// 非递归方式实现排序
fun sort(array: IntArray, startIndex: Int, endIndex: Int){
    val quickSortStack = Stack<Map<String,Int>>()
    // 整个数列的起止下标,以哈希的形式入栈
    val rootParam = HashMap<String,Int>()
    rootParam["startIndex"] = startIndex
    rootParam["endIndex"] = endIndex
    quickSortStack.push(rootParam)

    while (quickSortStack.isNotEmpty()){
        val param = quickSortStack.pop()
        val pivotIndex = partitionWithDouble(array,param["startIndex"]?:0,param["endIndex"]?:0)
        if(param["startIndex"]?:0< pivotIndex-1){
            val leftParam = HashMap<String,Int>()
            leftParam["startIndex"] = param["startIndex"]?:0
            leftParam["endIndex"] = pivotIndex-1
            quickSortStack.push(leftParam)
        }
        if(pivotIndex+1 < param["endIndex"]?:0){
            val rightParam = HashMap<String,Int>()
            rightParam["startIndex"] = pivotIndex+1
            rightParam["endIndex"] = param["endIndex"]?:0
            quickSortStack.push(rightParam)
        }
    }
}
复制代码

其实和上面的递归逻辑几乎一致,不同的地方是对栈的封装,以及在这里的判断,避免栈的无限循环。咱们接下来测试看看?

fun quickSort(){
    var array = intArrayOf(4,6,5,2,8,9,7,5,1,3)
    QuickSort.sort(array,0,array.size-1,false)
    println(array.contentToString())
    array = intArrayOf(4,8,6,7,4,2,1,5,3,9,5)
    QuickSort.sort(array,0,array.size-1,true)
    println(array.contentToString())
    array = intArrayOf(8,4,3,9,5,7,2,6,5,1,8,4)
    QuickSort.sort(array,0,array.size-1)
    println(array.contentToString())
}
复制代码

测试结果:

[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 8, 9]
复制代码

3.什么是堆排序?

堆排序 指的是利用最大(小)堆的特性,每次“删除”堆顶元素,而后经过堆的自我调整,接着再“删除”堆顶元素,直至堆元素被“删除”完了,这样被“删除”元素就是一个有序数组。

代码实现:

/**
 * <p>文件描述:堆排序的实现<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/11/02 0027 <p>
 * <p>@update 2019/11/02 0027<p>
 * <p>版本号:1<p>
 *
 */
object HeapSort {
    fun sort(array: IntArray) {
        // 把无序数组构建成堆
        for (i in (array.size - 2) / 2 downTo 0) {
            downAdjust(array, i, array.size, false)
        }
//        BinaryHeap.upAdjust(array)
        // 循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶
        for (i in array.size - 1 downTo 1) {
            // 最后一个元素和第一个元素交换
            val tmp = array[i]
            array[i] = array[0]
            array[0] = tmp
            // 下沉调整最大堆
            downAdjust(array, 0, i, false)
        }
    }

    // “下沉”调整 (删除节点)
    // isMax true 最大堆 false 最小堆
    private fun downAdjust(array: IntArray, index: Int, length: Int, isMax: Boolean) {
        var parentIndex = index
        var temp = array[parentIndex]
        var childIndex = 2 * parentIndex + 1
        while (childIndex < length) {
            if (isMax) {
                if (childIndex + 1 < length && array[childIndex + 1] > array[childIndex]) {
                    childIndex++
                }
                if (temp >= array[childIndex]) {
                    break
                }
            } else {
                if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
                    childIndex++
                }
                if (temp <= array[childIndex]) {
                    break
                }
            }

            array[parentIndex] = array[childIndex]
            parentIndex = childIndex
            childIndex = 2 * childIndex + 1
        }
        array[parentIndex] = temp
    }
}
复制代码

测试一波:

fun heapSort(){
    val array = intArrayOf(2,4,1,5,3,6,7,8,9)
    HeapSort.sort(array)
    println(array.contentToString())
}
复制代码

测试结果:

[9, 8, 7, 6, 5, 4, 3, 2, 1]
复制代码

特别须要注意的是,将数组构建成最小堆是降序,构建成最大堆是升序,且构建后“删除”堆顶元素后自我调整的时候也要遵循前面的规则。

4.计数排序

计数排序是一个非基于比较的排序算法,它的优点在于在对必定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。步骤以下:

  1. 计算数列的最大值
  2. 根据最大值肯定统计数组长度
  3. 遍历数列,填充统计数组
  4. 遍历统计数组 输出结果
图片源自https://www.itcodemonkey.com/article/11750.html

代码实现:

/**
 * <p>文件描述:计数排序的实现<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/11/2 0002 <p>
 * <p>@update 2019/11/2 0002<p>
 * <p>版本号:1<p>
 *
 */
object CountSort {

    fun sort(array: IntArray):IntArray{
        // 计算数列的最大值
        var max = array[0]
        for (i in 1 until array.size){
            if(array[i]>max){
                max = array[i]
            }
        }
        // 根据最大值肯定统计数组长度
        val countArray = IntArray(max+1)
        // 遍历数列,填充统计数组
        for(i in array){
            countArray[i]++
        }
        // 遍历统计数组 输出结果
        var index = 0
        val resultData = IntArray(array.size)
        for (i in countArray.indices){
            for (j in 0 until countArray[i]){
                resultData[index++] = i
            }
        }
        return resultData
    }
}
复制代码

测试一波:

fun countSort(){
    println(CountSort.sort(intArrayOf(8,4,5,2,1,5,6,2,5,9,4,7,1,6,2,3,4,5)).contentToString())
}
复制代码

测试结果:

[1, 1, 2, 2, 2, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 8, 9]
复制代码

看来确实挺快的,可是好像还有优化的空间,就是统计数组的长度,好比999,990,992,994,996,997,992,998,999,993,996,咱们的统计数组长度彻底有必要进一步优化的,下面是代码实现:

// 计数排序 优化
fun sort2(array: IntArray):IntArray{
    // 计算数列的最大值、最小值、以及差值
    var max = array[0]
    var min = array[0]
    for (i in 1 until array.size){
        if(array[i]>max){
            max = array[i]
        }
        if(array[i]<min){
            min = array[i]
        }
    }
    val d = max-min

    // 根据最大值肯定统计数组长度
    val countArray = IntArray(d+1)
    // 遍历数列,填充统计数组
    for(i in array){
        countArray[i-min]++
    }
    for (i in 1 until countArray.size){
        countArray[i]+=countArray[i-1]
    }

    // 遍历统计数组 输出结果
    val resultData = IntArray(array.size)
    for (i in array.size-1 downTo 0){
        resultData[countArray[array[i]-min]-1] = array[i]
        countArray[array[i]-min]--
    }
    return resultData
}
复制代码

测试一波:

fun countSort(){
    println(CountSort.sort(intArrayOf(8,4,5,2,1,5,6,2,5,9,4,7,1,6,2,3,4,5)).contentToString())
    println(CountSort.sort2(intArrayOf(99,110,115,105,107,92,111,106,98,108)).contentToString())
}
复制代码

测试结果:

[1, 1, 2, 2, 2, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 8, 9]
[92, 98, 99, 105, 106, 107, 108, 110, 111, 115]
复制代码

注意

  • 若是最大值和最小值之间的差值过大时,不建议使用,如2,4,7,10000000000
  • 当元素不是整数,也不适合,如10.2,11.5,12,13.5,14.3,由于非整数没法建立统计数组

5.桶排序(Bucket sort

桶排序 是一个排序算法,工做的原理是将数组分到有限数量的桶子里。每一个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。

以上内容来自百度百科

以上演示图源自https://blog.csdn.net/developer1024/article/details/79770240

若是对概念和演示图都看不懂,那也没有关系,咱们还有代码。写了这么多之后,发现以前本身原本不怎么熟悉的思路,如今跟着代码一遍一遍的运行下来,基本上都知道是怎么一回事了。言归正传,咱们看代码:

/**
 * <p>文件描述:桶排序的实现<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/11/2 0002 <p>
 * <p>@update 2019/11/2 0002<p>
 * <p>版本号:1<p>
 *
 */
object BucketSort {
    fun sort(array: DoubleArray):DoubleArray{
        // 获得数列的最大值、最小值、差值
        var max = array[0]
        var min = array[0]
        for (i in 1 until array.size){
            if(array[i]>max){
                max = array[i]
            }
            if(array[i]<min){
                min = array[i]
            }
        }
        val d = max-min
        // 初始化桶
        val bucketNumber = array.size
        val buicketList = mutableListOf<LinkedList<Double>>()
        for (i in 0 until bucketNumber){
            buicketList.add(LinkedList())
        }
        // 遍历原始数组
        for (i in array){
            val num = ((i-min)*(bucketNumber-1)/d).toInt()
            buicketList[num].add(i)
        }
        // 对每一个桶内部进行排序
        for (i in buicketList){
            // JDK 底层采用了归并排序或者归并的优化版本
            i.sort()
        }

        // 输出所有元素
        val resultData = DoubleArray(array.size)
        var index = 0
        for (list in buicketList){
            for (i in list){
                resultData[index] = i
                index++
            }
        }
        return resultData
    }
}
复制代码

测试一波:

// 桶排序
fun bucketSort(){
    println(BucketSort.sort(doubleArrayOf(3.2,4.2,6.5,5.1,15.2,11.5,12.6,4.8,6.4,9.9,1.5,10.2)).contentToString())
}
复制代码

测试结果:

[1.5, 3.2, 4.2, 4.8, 5.1, 6.4, 6.5, 9.9, 10.2, 11.5, 12.6, 15.2]
复制代码

上面的思路其实也是分治法的思路,只是有本身的发挥。

其实无论什么算法,都有自身的局限性,不一样的数据有不一样的算法更适合它,并无一招鲜吃遍天的招式。咱们选择具体的算法时,应该充分考虑到平均时间复杂度、最坏时间复杂度,空间复杂度,是否稳定排序(即顺序在能不调换的时候则不调换),而后有针对性的选择最合适的算法。

3、面试中的算法

前面学习的东西仍是基础知识,而面试中的算法就是对基础知识的灵活运用,下面来看看这些问题?

1.判断链表是否有环?

有一个单向链表,链表中有可能出现“环”,以下图。问题:如何用代码判断该链表是否有环?如何用代码求出环的长度以及入环点?

通过观察发现,链表的每个元素都是不重复的,这样咱们就能够经过穷举遍历哈希表缓存来实现,但这些都不是最优的方案,咱们直接使用快慢指针来判断链表是否有环,代码以下:

/**
 * <p>文件描述:面试算法代码<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/11/4 0004 <p>
 * <p>@update 2019/11/4 0004<p>
 * <p>版本号:1<p>
 *
 */

data class Node(var data:Int,var next:Node? = null)
object InterViewCode {
    // 链表是否有环
    fun isCycle(head:Node):Boolean{
        var p1:Node? = head
        var p2:Node? = head
        while (p1 != null && p2?.next != null){
            p1 = p1.next
            p2 = p2.next?.next
            if(p1 == p2){
                return true
            }
        }
            return false
    }
}
复制代码

测试代码:

fun testLinkedCycle(){
    val node1 = Node(5)
    node1.next = Node(3)
    node1.next?.next = Node(7)
    node1.next?.next?.next = Node(2)
    node1.next?.next?.next?.next = Node(6)
    node1.next?.next?.next?.next?.next = node1.next
    println("链表是否有环:${InterViewCode.isCycle(node1)}")
}
复制代码

测试结果:

链表是否有环:true
复制代码

其实回头来分析原理很简单,就比如龟兔赛跑,以前兔子一直在前面跑,若是赛道是环形的话,那么当兔子跑完第一圈之后,必定会看到乌龟;若是兔子一直跑完也没有看到乌龟,那么他们的赛道必定是非环形的。这个具体看看代码就明白了,代码中兔子的速度设置为乌龟的两倍,固然你也能够设置三倍。

接下来咱们须要考虑第二个问题:如何求出环的长度? 分析一下:

经过上面的龟兔赛跑的龟兔见面,证实了赛道 链表是闭环,若是龟兔第二次见面的话,证实这个过程兔子又跑完了一个整圈了,咱们计算这个过程兔子跑的长度就是环的长度了!计算一下:

// 获取链表环的长度
fun getCycleOfLength(head:Node):Int{
    var p1:Node? = head
    var p2:Node? = head
    var count = 0
    var length = 0
    while (p1 != null && p2?.next != null){
        p1 = p1.next
        p2 = p2.next?.next
        if(p1 == p2){
           count++
        }
        if (count ==1){
            length+=1
        }
        if(count == 2){
            return length
        }
    }
    return 0

}
复制代码

测试一下:

fun testLinkedCycle(){
    val node1 = Node(5)
    node1.next = Node(3)
    node1.next?.next = Node(7)
    node1.next?.next?.next = Node(2)
    node1.next?.next?.next?.next = Node(6)
    node1.next?.next?.next?.next?.next = node1.next
    println("链表是否有环:${InterViewCode.isCycle(node1)}")
    println("链表环的长度:${InterViewCode.getCycleOfLength(node1)}")
}
复制代码

测试结果:

链表是否有环:true
链表环的长度:4
复制代码

回头来分析一下:

兔子的速度比乌龟快1倍,即乌龟走一步兔子走两步,速度差为1步,当龟兔再次相遇时,兔子老兄已经比乌龟老弟多跑了一圈了,这里能够推理出一个公式:

环长 = 速度差*前进次数

回头再来看代码,这样就容易理解是怎么一回事了!而后咱们再来看看最后一个问题:求入环点是多少?

从上图推算得知,当龟兔第一次相遇时:

  • 乌龟跑的奔跑的距离是D+s1
  • 此时兔子奔跑的距离是D+(S1+S2)+S1
  • 根据兔子速度是乌龟的两倍得出:2*(D+S1) = D+(S1+S2)+S1D = S2

通过上面的分析,咱们尝试在第一次相遇之后,将兔子以速度太快罚到起点从新跑,这个时候兔子内心有委屈,因而和乌龟速度保持同步,而后咱们验证一下它们再次相遇的点是否是起点?是的话,就证实咱们上面的推论正确。代码以下:

// 获取链表环的起点
fun getCycleOfStart(head:Node):Int{
    var p1:Node? = head
    var p2:Node? = head
    var count = 0

    while (p1 != null && p2?.next != null){
        // 这是乌龟 速度始终保持
        p1 = p1.next
        // 这是兔子 速度在相遇前和相遇后有变化
        // 相遇之后慢慢跑,和乌龟速度同步
        p2 = if(count == 1){
           p2.next
        }else{
            // 还在首圈冲刺阶段,开足马力
            p2.next?.next
        }

        if(p1 == p2){
            count++
            if(count == 1){
                // 从头开始
                p1 = head
            }else{
                return p1?.data?:0
            }
        }


    }
    return 0

}
复制代码

测试代码:

fun testLinkedCycle(){
    val node1 = Node(5)
    node1.next = Node(3)
    node1.next?.next = Node(7)
    node1.next?.next?.next = Node(2)
    node1.next?.next?.next?.next = Node(6)
    node1.next?.next?.next?.next?.next = node1.next
    println("链表是否有环:${InterViewCode.isCycle(node1)}")
    println("链表环的长度:${InterViewCode.getCycleOfLength(node1)}")
    println("链表环的入环点:${InterViewCode.getCycleOfStart(node1)}")
}
复制代码

测试结果:

链表是否有环:true
链表环的长度:4
链表环的入环点:3
复制代码

OK ,龟兔赛跑结束了,不分输赢,哈哈!

2.一场关于栈的面试

实现一个栈,该栈带有出栈(pop)、入栈(push)、取最小元素(getMin)三个方法,要保证三个方法的时间复杂度都是(O(1))

解题思路:加一个备胎

实现代码:

/**
 * <p>文件描述:自定义栈<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/11/4 0004 <p>
 * <p>@update 2019/11/4 0004<p>
 * <p>版本号:1<p>
 *
 */
class CustomStack {
    private val mainStack = Stack<Int>()
    private var minStack = Stack<Int>()

    // 入栈
    fun push(element:Int){
        mainStack.push(element)
        if(minStack.empty() || element <= minStack.peek()){
            minStack.push(element)
        }
    }

    // 出栈
    fun pop():Int{
        if(mainStack.peek() == minStack.peek()){
            minStack.pop()
        }
        return mainStack.pop()
    }

    // 获取栈最小元素
    fun getMin():Int{
        if(mainStack.empty()){
            throw Exception("Stack is empty")
        }
        return minStack.peek()
    }
}
复制代码

测试效果:

fun testMinStack(){
    val stack = CustomStack()
    stack.push(5)
    stack.push(7)
    stack.push(8)
    stack.push(3)
    stack.push(1)
    println(stack.getMin())
    stack.pop()
    stack.pop()
    stack.pop()
    println(stack.getMin())
}
复制代码

测试结果:

1
5
复制代码

3.如何求出最大公约数

写一段代码,求出两个整数的最大公约数,要尽可能优化算法的性能。

看到这道题的时候,我记起了小学五年级学的质数与最大公约数的概念,因而很快的写出了本身的答案:

// 获取最大公约数
fun getGreatCommonDivisor(a:Int,b:Int,c:Int = 1):Int{
    val max = kotlin.math.max(a, b)
    val min = min(a,b)
    if(max % min == 0){
        return min
    }
    var common = c
    var total = min
    for (i in 2..min){
        if(i >= total)break
        if(isPrime(i)){
            if(min % i == 0 && max % i == 0){
                common *= i
                total /= common
                return getGreatCommonDivisor(max,total,common)
            }
        }
    }
    return common
}

// 是不是素数
private fun  isPrime(data:Int):Boolean {
    if(data<4){
        return true
    }
    if(data%2 == 0){
        return false
    }

    val sqrt = sqrt(data.toDouble())
    for (i in 3..sqrt.toInt() step 2){
        if(data%i == 0){
            return false
        }
    }

    return true;
}
复制代码

测试代码:

fun getMax(){
    val result = InterViewCode.getGreatCommonDivisor(240,600)
    println(result)
}
复制代码

测试结果:

120
复制代码

我兴致高昂的计算出答案的时候,再一看后面的内容,发现本身太单纯了!原来我解题的思路是经过公式硬算,和暴力枚举差很少是一模一样,只不过个人方法更靠谱一些。而这道题还有更简单而且是我不知道的公式而已:

展转相除法 也称为欧几里得算法,指两个整数ab(a>b),它们的最大公约数等于a除以b的余数cb之间的最大公约数

看上去好像比较绕,举个栗子:246060除以2412,而后2412的最大公约数等于12,那么2464的最大公约数就是12

**注意:**当两个整数较大时,作a % b取模运算的性能会比较差。

// 展转相除法
    fun getGreatCommonDivisor2(a:Int,b:Int):Int{
        val max = kotlin.math.max(a, b)
        val min = min(a,b)
        if(max%min == 0){
            return min
        }
        return getGreatCommonDivisor2(max%min,min)
    }
复制代码

更相减损术 出自我国的《九章算术》,指两个整数ab(a>b),它们的最大公约数等于a-b的差值c和较小数b的最大公约数

注意: 这里仍是有个问题,虽然没有取模运算性能差的问题,可是运算效率低,毕竟相减没有相除快!

// 更相减损术
fun getGreatCommonDivisor3(a:Int,b:Int):Int{
    if(a == b)return a
    val max = kotlin.math.max(a, b)
    val min = min(a,b)
    if(max % min == 0){
        return min
    }
    return getGreatCommonDivisor2(max - min,min)
}
复制代码

移位运算 公式推理过程就再也不分析了,我们直接看代码:

// 位移运算
    fun getGreatCommonDivisor4(a:Int,b:Int):Int{
        if(a == b)return a
        if((a and 1) == 0 && (b and 1) == 0 ){
            return getGreatCommonDivisor4(a.shr(1),b.shr(1)).shl(1)
        }else if((a and 1) == 0 && (b and 1) != 0){
            return getGreatCommonDivisor4(a.shr(1),b)
        }else if((a and 1) != 0 && (b and 1) == 0){
            return getGreatCommonDivisor4(a,b.shr(1))
        }else{
            val max = kotlin.math.max(a, b)
            val min = min(a,b)
            return getGreatCommonDivisor4(max-min,min)
        }
    }
复制代码

测试代码:

// 求最大公约数
    fun getMax(){
        // 默认方法
        println(InterViewCode.getGreatCommonDivisor(240,600))
        // 展转相除法
        println(InterViewCode.getGreatCommonDivisor2(240,600))
        // 更相减损术
        println(InterViewCode.getGreatCommonDivisor3(240,600))
        // 位移运算
        println(InterViewCode.getGreatCommonDivisor4(240,600))

    }
复制代码

测试结果:

120
120
120
120
复制代码

说实话,若是遇到这样的面试题,我确定不知道怎么答?毕竟哪些公式不是逻辑思惟,而是前人推理无数次以后直接拿出来的结论,不多人会记得清楚这些公式。因此,我以为知道这些就好,不必定须要死记硬背。

4.如何判断一个数是否为2的整数次冥?

实现一个方法,来判断一个正整数是不是2的整数次冥,要求性能尽量高。

推理过程就不说了,反正我深深相信位运算太强大了!

// 求是不是2的整数次冥
    fun isPowerOf(num: Int): Boolean {
        return num and num - 1 == 0
    }
复制代码

5.无序数组排序后的最大相邻差

有一个无序整数型数组,如何求出改数组排序后任意两个相邻元素的最大差值?要求时间和空间复杂度最优。

解题思路是经过计数排序或者桶排序来排序的同时,顺便把相邻元素的最大差值一并求出来。

// 求无序数组最大相邻差
    fun getMaxSortedDistance(array: IntArray):Int{
        var max = array[0]
        var min = array[0]
        // 计算数列的最大值和最小值以及差值
        for (i in array){
            if(i>max){
                max = i
            }
            if(i < min){
                min = i
            }
        }
        val d = max-min
        if(d == 0)return 0
        // 初始化桶
        val bucketNum = array.size
        val bucketList = Array(bucketNum) { _ ->
            return@Array Bucket()
        }
        // 遍历原始数组 肯定桶的大小
        for (i in array){
            // 肯定数组元素所归属的桶下标
            val index = (i-min)*(bucketNum-1)/d
            if(bucketList[index].min == 0 || bucketList[index].min > i){
                bucketList[index].min = i
            }
            if(bucketList[index].max == 0 || bucketList[index].max<i){
                bucketList[index].max = i
            }
        }
        // 遍历桶,找到最大值
        var leftMax = bucketList[0].max
        var result = 0
        for (i in 1 until bucketNum){
            if(bucketList[i].min == 0)continue
            if(bucketList[i].min - leftMax > result){
                result = bucketList[i].min - leftMax
            }
            leftMax = bucketList[i].max
        }
        return result
    }
复制代码

测试代码:

// 求最大差
    fun getMaxDistance(){
        println("最大相邻差:${InterViewCode.getMaxSortedDistance(intArrayOf(2,6,3,4,5,10,9))}")
    }
复制代码

测试结果:

最大相邻差:3
复制代码

6.如何用栈实现队列

用栈来模拟一个队列,要求实现两个基本的操做:入队、出队

老规矩,用两个栈来实现,其中一个栈做为备胎:

/**
 * <p>文件描述:用栈实现自定义队列<p>
 * <p>@author 烤鱼<p>
 * <p>@date 2019/11/4 0004 <p>
 * <p>@update 2019/11/4 0004<p>
 * <p>版本号:1<p>
 *
 */
class CustomQueue {
    private val stackA = Stack<Int>()
    private val stackB = Stack<Int>()

    fun push(element:Int){
        stackA.push(element)
    }

    fun pop():Int{
        if(stackB.isEmpty()){
            if(stackA.empty()){
                throw Exception("Queue is empty!")
            }
            transfer()
        }
        return stackB.pop()
    }

    private fun transfer(){
        while (stackA.isNotEmpty()){
            stackB.push(stackA.pop())
        }
    }
}
复制代码

测试代码:

fun testCustomQueue(){
        val customQueue = CustomQueue()
        customQueue.push(1)
        customQueue.push(2)
        customQueue.push(3)
        println(customQueue.pop())
        println(customQueue.pop())
        customQueue.push(4)
        println(customQueue.pop())
        println(customQueue.pop())
    }
复制代码

测试结果:

1
2
3
4
复制代码

7.寻找全排列的下一个数

给出一个正整数,找出这个正整数全部数字全排列的下一个数字。如:

若是输入12345,返回12354

若是输入12354,返回12435

若是输入12435,返回12453

8.一道关于数字的题目

给出一个整数,从该整数中去掉k个数字,要求剩下的数字形式的新整数尽量小。

9.如何实现大整数相加?

给出两个很大的整数(假设有100位),要求实现两个整数的和。

10.如何求解金矿问题

好久好久之前,有一位国王拥有五座金控,每座金矿的黄金储量不一样,须要参与挖掘的工人人数也不一样,例若有的金矿储量是500KG黄金,须要三我的挖掘......

若是参与挖矿的工人总数是10人,每座金矿要么全挖,要么不挖,不能派出一半的人挖去一半的金矿。要求用程序求出,要想获得尽量多的黄金,应该选择挖取哪几座金矿?

11.寻找缺失的整数

在一个无序数组里有99个不重复的正整数,范围是1~100,惟独缺乏11~100中的整数,如何找出这个缺失的整数?

4、算法在项目中的应用(待完成)


源码:传送门

相关文章
相关标签/搜索