《漫畫算法——小灰的算法之旅》讀後筆記

看完了《漫畫算法》第一遍,以爲有些意猶未盡,所以第二次重溫時,打算開始作一些筆記,這樣能夠加深本身對算法知識的理解,也能對沒有看過這本書的朋友提供一些幫助。可是具體有幾篇筆記,這個須要一邊看,一邊寫,儘可能在雙十一以前完成本次觀後感的總結吧!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、算法在項目中的應用(待完成)


源碼:傳送門

相關文章
相關標籤/搜索