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

聲明:本篇博客是博主閱讀《漫畫算法 小灰的算法之旅》所做的筆記,僅供學習,侵刪,嚴禁轉載!

(一)、數據結構

一、數組

基本操作

  1. 讀取元素 O(1)

    根據數組下標讀取元素 --隨機讀取

  2. 更新元素 時間複雜度 O(1)

  3. 插入元素

    • 尾部插入

    • 中間插入 O(n)

      從數組的中間插入數據,後面的數需要依次後移

      //lenth 數組實際內容長度
      public void insertArray(int []array, int lenth,int insertIndex,int element){
          if(lenth > insertIndex || insertIndex < 0){
              throw new IndexOutOfBoundsException("超出數組實際元素範圍")
          }
          for(int i = lenth;i>insertIndex;i--){
              array[i] = array[i-1]
          }
          array[insertIndex]=element;
          lenth++;
      }
      
    • 超範圍插入 O(n)+O(n)=O(n)

      數組已滿,插入數據的時候,可以把數組轉移到另外一個2倍大的數組裏,再進行中間插入

  4. 刪除元素 O(n)

    如果刪除的元素位於數組中間,則需要把後面的元素向前挪動一位

    /* array 數組  size 數組實際最後一個元素所在的下標 index 刪除的元素所在的下標 */
    public int delete(int index){
        if(index<0||index > size){
            return error;
        }
        int deleteElement = array[index];
        for(int i = index;i<size-1;i++){
            array[i]=array[i+1];
        }
        size--;
        return deleteElement;
    }
    

    另一種操作方法:將數組的最後一個元素複製到將要刪除的元素所在的位置,然後刪除最後一個元素,當然這不是刪除元素的主流操作方式。

優缺點

  • 非常高效的隨機訪問能力(二分查找)
  • 插入刪除操作不便
  • 數組適合讀操作多,寫操作少的場景

二、鏈表

  • 單向鏈表

    private static class Node{
        int data;
        Node next;
    }
    
  • 雙向鏈表

    private static class Node{
        int data;
        Node prev;
        Node next;
    }
    
  • 鏈表是隨機存儲的

基本操作

  • 查找節點

    從頭節點開始遍歷,直到查找到目標節點,最壞的時間複雜度是O(n)

  • 更新節點

    查找到節點後,對數據進行更新

  • 插入節點

    • 尾部插入

      把最後一個節點的next指針指向新的節點

    • 頭部插入

      把新節點的next指針指向頭節點,把新節點變爲鏈表的頭節點

    • 中間插入

      新節點的next指針指向插入位置的節點

      插入位置的前置節點的next指針指向新的節點

  • 刪除元素

    • 尾部刪除

      把倒數第二節點的next指針指向空

    • 頭部刪除

      把頭節點的next指針指向的節點設置爲頭節點

    • 中間刪除

      把刪除位置的前置節點的next指針指向刪除位置的下一個節點

java有垃圾回收機制,對於沒有外部引用指向的節點,會被自動回收。

如果不考慮插入、刪除之前查找元素的過程,只考慮純粹的插入和刪除操作,時間複雜度都是O(1)

public class NodeList {
    private static class Node{
        int data;
        Node next;
        Node(int data){
            this.data = data;
        }
    }
    //頭節點
    private Node head;
    //尾部節點
    private Node last;
    //鏈表實際長度
    private int size;

    //根據data的值尋找節點
    public Node find(int data){
        Node findNode = head;
        while (findNode!=null){
            if(findNode.data == data){
                return findNode;
            }else{
                findNode = findNode.next;
            }
        }
        return null;
    }

    //根據index查找第幾個節點
    public Node findIndex(int index) throws Exception{
        if(index<0||index>=size){
            throw new IndexOutOfBoundsException("超出鏈表節點範圍");
        }
        Node temp = head;
        for(int i=0;i<index;i++){
            temp = temp.next;
        }
        return temp;
    }

    //插入節點
    public boolean insert(int data,int index)throws Exception{
        if(index<0||index>size){
            throw new IndexOutOfBoundsException("超出鏈表節點範圍");
        }
        Node nodeInsert = new Node(data);
        if(size==0){
            //鏈表爲空
            this.head = nodeInsert;
            this.last = nodeInsert;
        }else if(index==0){
            //插入的位置爲頭結點
            nodeInsert.next = this.head;
            this.head = nodeInsert;
        }else if(index==size){
            //插入的位置爲尾節點
            this.last.next = nodeInsert;
            this.last = nodeInsert;
        }else{
            //插入的位置爲中間位置
            Node preNode = findIndex(index-1);
            nodeInsert.next = preNode.next;
            preNode.next=nodeInsert;
        }
        size++;
        return true;
    }

    //刪除節點
    public Node delete(int index)throws Exception{
        if(index<0||index>=size){
            throw new IndexOutOfBoundsException("超出鏈表節點範圍");
        }
        Node removeNode = null;
        if(index==0){
            //刪除頭結點
            removeNode = this.head;
            this.head = this.head.next;
        }else if(index == size-1){
            //刪除尾節點
            Node prevNode = findIndex(size-1);
            removeNode = this.last;
            prevNode.next = null;
            this.last = prevNode;
        }else{
            //刪除中間節點
            Node prevNode = findIndex(index-1);
            removeNode = prevNode.next;
            prevNode.next = prevNode.next.next;
        }
        size--;
        return removeNode;
    }
}

優缺點

數組與鏈表的對比

查找 更新 插入 刪除
數組 O(1) O(1) O(n) O(n)
鏈表 O(n) O(1) O(1) O(1)
  • 更加靈活地進行插入和刪除操作
  • 適合在尾部頻繁插入、刪除元素

三、棧

  • 先入後出

    最早進入的元素存放的位置是棧底

    最後進入的元素存放的位置是棧頂

  • 基本操作 O(1)

    • 入棧
    • 出棧

四、隊列

  • 先入先出 FIFO

    出口端叫做隊頭

    入口端叫做隊尾

    用數組實現時,爲了入隊操作的方便,把隊尾的位置規定爲最後入隊元素的下一個位置。

  • 基本操作 O(1)

    • 入隊
    • 出隊

    用數組實現的隊列可以使用循環隊列的方式來維持隊列容量的恆定。

    (隊尾下標+1)%數組長度=隊頭下標時,代表隊列已滿,隊尾指針指向的位置永遠空出一位

    所以隊列最大容量=數組長度-1

public class MyQueue {
    private int []array;
    private int front;
    private int rear;
    public MyQueue(int size){
        this.array = new int[size];
    }
    //入隊
    public void inQueue(int value)throws Exception{
        if((rear+1)%array.length == front){
            throw new Exception("隊列已滿");
        }
        array[rear]=value;
        rear = (rear+1)%array.length;
    }
    //出隊
    public int outQueue()throws Exception{
        if(rear == front){
            throw new Exception("隊列已空");
        }
        int outValue = array[front];
        front=(front+1)%array.length;
        return outValue;
    }
    //輸出隊列
    public void outPutQueue(){
        for (int i=front;i!=rear;i=(i+1)%array.length){
            System.out.println(array[i]);
        }
    }
}

棧和隊列的應用

  • 實現遞歸的邏輯,可以使用棧來代替,因爲棧可以回溯方法,還有一個著名的應用場景是 麪包屑導航 ,使用戶在瀏覽頁面時可以輕鬆地回溯到上一級或者更上一級頁面。
  • 隊列應用到多線程中,爭奪公平鎖的等待隊列。網絡爬蟲把待爬取的網站url存入隊列中。
  • 雙端隊列 deque 隊頭隊尾均可入隊或出隊
  • 優先隊列 基於二叉堆來實現,誰的優先級高誰優先出隊

五、散列表/哈希表 hash table

寫操作

在散列表中插入鍵值對

  • 通過哈希函數,把key轉換成數組下標
  • 保存到數組裏

哈希衝突

  • 開放尋址法

    尋址當前元素的下一個位置是否爲空,爲空則可以佔用

  • 鏈表法

    哈希數組的每一個元素不僅是一個Entry對象,還是一個鏈表的頭節點,當有衝突試,插入到對應的鏈表中即可。

讀操作

  • 通過哈希函數,把key轉換成數組下標x
  • 在數組中的下標x處找對應的元素

擴容

散列表會在多次插入後達到一定飽和度,key映射位置發生衝突的概率會提高

步驟

  • 擴容,創建一個新的entry空數組,長度是原來的2倍
  • 重新hash,擴容後hash規則會隨之改變,把所有的entry重新hash到新的數組中。

六、二叉樹

  • 滿二叉樹
  • 完全二叉樹

二叉樹的數組存儲

父節點的下標是parent,則其左孩子節點的下標是2*parent+1,右孩子節點下標是2*parent+2

對於稀疏的二叉樹來說,用數組表示法是非常浪費空間的。

二叉樹的應用

二叉查找樹
  • 如果左子樹不爲空,則左子樹上所有節點的值均小於根節點的值
  • 如果右子樹不爲空,則右子樹上所有節點的值均大於根節點的值
  • 左右子樹也都是二叉查找樹
二叉排序樹

​ 二叉排序樹也就是二叉查找樹,新插入節點時根據上述規則來插入數據,但是會造成二叉樹不平衡。解決不平衡的方式:紅黑樹,AVL樹,樹堆。

二叉樹的遍歷

1.深度優先遍歷
  • 前序遍歷

    根節點->左子樹->右子樹

  • 中序遍歷

    左子樹->根節點->右子樹

  • 後序遍歷

    左子樹->右子樹->根節點

public class LinkTree {
    private static class TreeNode{
        int data;
        TreeNode leftChild;
        TreeNode rightChild;
        TreeNode(int data){
            this.data = data;
        }
    }
    //構建二叉樹
    public static TreeNode createBinaryTree(LinkedList<Integer> inputList){
        TreeNode node = null;
        if(inputList==null||inputList.isEmpty()){
            return null;
        }
        Integer data = inputList.removeFirst();
        if(data != null){
            node = new TreeNode(data);
            node.leftChild = createBinaryTree(inputList);
            node.rightChild = createBinaryTree(inputList);
        }
        return node;
    }

    //遞歸深度優先遍歷
    //前序遍歷
    public static void preOrderTraveral(TreeNode node){
        if(node==null) return;
        System.out.println(node.data);
        preOrderTraveral(node.leftChild);
        preOrderTraveral(node.rightChild);
    }
    //中序遍歷
    public static void inOrderTraveral(TreeNode node){
        if(node==null) return;
        inOrderTraveral(node.leftChild);
        System.out.println(node.data);
        inOrderTraveral(node.rightChild);
    }
    //後序遍歷
    public static void postOrderTraveral(TreeNode node){
        if(node==null) return;
        postOrderTraveral(node.leftChild);
        postOrderTraveral(node.rightChild);
        System.out.println(node.data);
    }
    //非遞歸深度優先遍歷
    //二叉樹非遞歸前序遍歷 V1
    public void preOrderTravelStackV1(TreeNode root){
        if(root==null) return;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode treeNode = root;
        while(treeNode!=null || !stack.isEmpty()){
            while (treeNode!=null){
                System.out.println(treeNode.data);
                stack.push(treeNode);
                treeNode=treeNode.leftChild;
            }
            if(!stack.isEmpty()){
                treeNode = stack.pop();
                treeNode = treeNode.rightChild;
            }
        }
    }
    //二叉樹非遞歸前序遍歷 V2
    public void preOrderTravelStackV2(TreeNode root){
        if(root==null) return;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode treeNode = root;
        while(treeNode!=null || !stack.isEmpty()){
            if(treeNode!=null){
                System.out.println(treeNode.data);
                stack.push(treeNode);
                treeNode = treeNode.leftChild;
            }else{
                treeNode = stack.pop();
                treeNode = treeNode.rightChild;
            }
        }
    }
    //二叉樹非遞歸前序遍歷 V3
    public void preOrderTravelStackV3(TreeNode root){
        if(root==null) return;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode treeNode = root;
        stack.push(treeNode);
        while(!stack.isEmpty()){
            System.out.println(treeNode.data);
            if(treeNode.rightChild!=null){
                stack.push(treeNode.rightChild);
            }
            if(treeNode.leftChild!=null){
                treeNode = treeNode.leftChild;
            }else{
                treeNode = stack.pop();
            }
        }
    }
    //二叉樹非遞歸中序遍歷 v1
    public void inOrderTravelStackV1(TreeNode root){
        if(root==null) return;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode treeNode = root;
        while(treeNode!=null || !stack.isEmpty()){
            while (treeNode!=null){
                stack.push(treeNode);
                treeNode=treeNode.leftChild;
            }
            if(!stack.isEmpty()){
                treeNode = stack.pop();
                System.out.println(treeNode.data);
                treeNode=treeNode.rightChild;
            }
        }
    }
    //二叉樹非遞歸中序遍歷 v2
    public void inOrderTravelStackV2(TreeNode root){
        if(root==null) return;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode treeNode = root;
        while (treeNode!=null || !stack.isEmpty()){
            if(treeNode==null){
                treeNode = stack.pop();
                System.out.println(treeNode.data);
                treeNode = treeNode.rightChild;
            }else{
                stack.push(treeNode);
                treeNode = treeNode.leftChild;
            }
        }
    }
    //二叉樹非遞歸後序遍歷 v1
    public void postOrderTravelStackV1(TreeNode root){
        Stack<TreeNode> stack1 = new Stack<TreeNode>();
        Stack<TreeNode> stack2 = new Stack<TreeNode>();
        TreeNode treeNode = root;
        stack1.push(treeNode);
        while(!stack1.isEmpty()){
            TreeNode cur = stack1.pop();
            stack2.push(cur);
            if(cur.leftChild!=null) stack1.push(cur.leftChild);
            if(cur.rightChild!=null) stack1.push(cur.rightChild);
        }
        while (!stack2.isEmpty()){
            System.out.println(stack2.pop().data);
        }
    }
    //二叉樹非遞歸後序遍歷 v2
    public void postOrderTravelStackV2(TreeNode root){
        if(root==null) return;;
        Stack<TreeNode> stack= new Stack<TreeNode>();
        TreeNode lastVisit=null;
        TreeNode treeNode = root;
        while (treeNode!=null){
            stack.push(treeNode);
            treeNode = treeNode.leftChild;
        }
        while (!stack.isEmpty()){
            treeNode = stack.pop();
            //走到這裏,treeNode 都是空的,並且已經遍歷到左子樹底端
            if(treeNode.rightChild == null || treeNode.rightChild == lastVisit){
                System.out.println(treeNode.data);
                lastVisit = treeNode;
            }else {
                //左子樹剛被訪問過,則需進入右子樹(根節點需再次入棧)
                stack.push(treeNode);
                treeNode = treeNode.rightChild;
                while (treeNode!=null){
                    stack.push(treeNode);
                    treeNode = treeNode.leftChild;
                }
            }
        }
    }
    public static void main(String[] args) {
        LinkedList<Integer> inputList = new LinkedList<Integer>(Arrays.asList(
                new Integer[]{
                        3,2,9,null,null,10,null,null,8,null,4
                }
        ));
        TreeNode treeNode = createBinaryTree(inputList);
        System.out.println("前序");
        preOrderTraveral(treeNode);
        System.out.println("中序");
        inOrderTraveral(treeNode);
        System.out.println("後序");
        postOrderTraveral(treeNode);
    }
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-W2Kezd9i-1580185021512)(imgs/binarytree.png)]

2.廣度優先遍歷

層序遍歷

//層序遍歷
public static void levelOrderTraversal(TreeNode root){
    Queue<TreeNode> queue = new LinkedList<TreeNode>();
    queue.offer(root);
    while (!queue.isEmpty()){
        TreeNode node = queue.poll();
        System.out.println(node.data);
        if(node.leftChild!=null){
            queue.offer(node.leftChild);
        }
        if(node.rightChild!=null){
            queue.offer(node.rightChild);
        }
    }
}

七、二叉堆

  • 最大堆

    任何一個父節點的值,都大於或等於它左右孩子節點的值

  • 最小堆

    任何一個父節點的值,都小於或等於它左右孩子節點的值

  • 堆頂

    二叉堆的根節點

  • 二叉堆的自我調整

    • 插入節點 O(logn)

      插入位置是完全二叉樹的最後一個位置,如果新節點比父節點的值小(對於最小堆來說)則讓新節點‘上浮’,和父節點交換位置。

    • 刪除節點 O(logn)

      刪除位置爲堆頂,把最後一個節點臨時補到原本堆頂的位置,當其比左右孩子節點中最小的一個還要大時(對於最小堆來說),則讓其‘下沉’,和最小的節點交換位置。

    • 構建二叉堆 O(n)

      把無序的完全二叉樹調整爲二叉堆,本質就是讓所有非葉子節點依次’下沉‘。

public class MyHeap {
    //二叉堆雖然是一個完全二叉樹,但其使用順序存儲,均存儲在數組中
    //‘上浮’調整
    public static void upAdjust(int[] array){
        int childIndex = array.length-1;
        int parentIndex = (childIndex-1)/2;
        //temp保存插入的葉子節點值,用於最後的賦值
        int temp = array[childIndex];
        while (childIndex>0 && temp<array[parentIndex]){
            //無須真正交換,單向賦值即可
            array[childIndex] = array[parentIndex];
            childIndex = parentIndex;
            parentIndex = (childIndex-1)/2;
        }
        array[childIndex]=temp;
    }
    //‘下沉’調整
    public static void downAdjust(int []array,int parentIndex,int length){
        //temp保存父節點值,用於最後的賦值
        int temp = array[parentIndex];
        int 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;
    }
    //構建堆
    public static void buildHeap(int []array){
        for(int i = (array.length-2)/2;i>=0;i--){
            downAdjust(array,i,array.length);
        }
    }

    public static void main(String[] args) {
        int[]array = new int[]{
                1,3,2,6,5,7,8,9,10,0
        };
        upAdjust(array);
        System.out.println(Arrays.toString(array));
        array = new int[]{
                7,1,3,10,5,2,8,9,6
        };
        buildHeap(array);
        System.out.println(Arrays.toString(array));
    }
}

八、優先隊列

  • 最大優先隊列

    無論入隊順序如何,都是當前最大的元素優先出隊

  • 最小優先隊列

    無論入隊順序如何,都是當前最小的元素優先出隊

  • 實現

    使用最大堆來實現最大優先隊列,那麼,每一次入隊就是堆的插入操作,每一次出隊,就是堆的刪除堆頂操作。

public class PriorityQueue {
    private int[] array;
    private int size;
    public PriorityQueue(){
        array = new int[32];
    }
    //入隊
    public void enQueue(int key){
        //隊列長度超出範圍,擴容
        if(size>=array.length){
            resize();
        }
        array[size++]=key;
        upAdjust();
    }
    //出隊
    public int deQueue()throws Exception{
        if(size<=0){
            throw new Exception("the queue is empty");
        }
        //獲取堆頂元素
        int head = array[0];
        //讓最後一個元素移動到堆頂
        array[0]=array[--size];
        downAdjust();
        return head;
    }
    //上浮調整
    private void upAdjust(){
        int childIndex = size-1;
        int parentIndex = (childIndex-1)/2;
        //temp 保存插入的葉子節點值,用於最後的賦值
        int  temp = array[childIndex];
        while (childIndex>0 && temp>array[parentIndex]){
            //無須真正交換,單向賦值即可
            array[childIndex]=array[parentIndex];
            childIndex=parentIndex;
            parentIndex=parentIndex/2;
        }
        array[childIndex]=temp;
    }
    //下沉調整
    private void downAdjust(){
        //temp 保存父節點的值,用於最後的賦值
        int parentIndex = 0;
        int temp = array[parentIndex];
        int 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;
    }
    //隊列擴容
    private void resize(){
        int newSize = this.size*2;
        this.array = Arrays.copyOf(this.array,newSize);
    }

    public static void main(String[] args) throws Exception {
        PriorityQueue priorityQueue = new PriorityQueue();
        priorityQueue.enQueue(3);
        priorityQueue.enQueue(5);
        priorityQueue.enQueue(10);
        priorityQueue.enQueue(2);
        priorityQueue.enQueue(7);
        System.out.println("出隊元素:"+priorityQueue.deQueue());
        System.out.println("出隊元素:"+priorityQueue.deQueue());
    }
}

(二)、排序算法

  • O(n^2)
    • 冒泡
    • 選擇排序
    • 插入排序
    • 希爾排序
  • O(nlogn)
    • 快速排序
    • 歸併排序
    • 堆排序
  • O(n)
    • 計數排序
    • 桶排序
    • 基數排序

一、冒泡排序

public class MySort {
    public static void bubbleSortV1(int array[]){
        for(int i=0;i<array.length-1;i++){
            for(int j=0;j<array.length-i-1;j++){
                int temp =0;
                if(array[j]>array[j+1]){
                    temp = array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                }
            }
        }
    }
    //添加flag提前停止排序
    public static void bubbleSortV2(int array[]){
        for(int i=0;i<array.length-1;i++){
            boolean isSorted = true;
            for(int j=0;j<array.length-i-1;j++){
                int temp =0;
                if(array[j]>array[j+1]){
                    temp = array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                    isSorted = false;
                }
            }
            if(isSorted) break;
        }
    }
    //添加無序數列的邊界,減少比對範圍
    public static void bubbleSortV3(int array[]){
        //記錄最後一次交換的位置
        int lastChange = 0;
        //無序數列的邊界,每次比較只需要比到這裏爲止
        int sortBorder = array.length-1;
        for(int i=0;i<array.length-1;i++){
            boolean isSorted = true;
            for(int j=0;j<sortBorder;j++){
                int temp =0;
                if(array[j]>array[j+1]){
                    temp = array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                    isSorted = false;
                    lastChange = j;
                }
            }
            sortBorder = lastChange;
            if(isSorted) break;
        }
    }
    public static void main(String[] args) {
        int []array = new int[]{
                5,8,6,3,9,2,1,7
        };
        bubbleSortV3(array);
        System.out.println(Arrays.toString(array));
    }
}

二、雞尾酒排序

  • 雞尾酒排序是冒泡排序的優化,元素比較和交換是雙向的
  • 適合數組中大多數元素有序的情況
  • 雞尾酒排序能在特定條件下減少排序的回合數,但是代碼量增多了
//雞尾酒排序
public static void cocktailSort(int array[]){
    int temp=0;
    for(int i =0; i<array.length/2;i++){
        boolean isSort = true;
        for(int j = 0;j<array.length-i-1;j++){
            if(array[j]>array[j+1]){
                temp=array[j];
                array[j]=array[j+1];
                array[j+1]=temp;
                isSort = false;
            }
        }
        if(isSort) break;
        isSort=true;
        for(int j=array.length-i-1;j>i;j--){
            if(array[j]<array[j-1]){
                temp=array[j];
                array[j]=array[j-1];
                array[j-1]=temp;
                isSort = false;
            }
        }
        if(isSort) break;
    }
}

三、快速排序

在每一輪挑選一個基準元素,把其他比他大的元素移動到數列一邊,把比他小的元素移動到數列的另一邊,從而把數列拆成兩個部分,再進行分治排序。

  • 雙邊循環法

    從數組的兩邊交替遍歷元素

//快速排序-雙邊循環法
public static void quickSort(int[]array,int startIndex,int endIndex){
    if(startIndex>=endIndex){
        return;
    }
    //得到基準元素
    int privotIndex = partitionDouble(array,startIndex,endIndex);
    //根據基準元素,分成兩部分進行遞歸排序
    quickSort(array,startIndex,privotIndex-1);
    quickSort(array,privotIndex+1,endIndex);
}
public static int partitionDouble(int[] array,int startIndex,int endIndex){
    //取第一個位置的元素作爲基準元素,也可以選擇隨機位置
    int pivot = array[startIndex];
    int left = startIndex;
    int right = endIndex;
    while (left!=right){
        //控制right指針比較並左移
        while (left<right&&array[right]>pivot){
            right--;
        }
        while (left<right&&array[left]<=pivot){
            left++;
        }
        if(left<right){
            int p = array[left];
            array[left]=array[right];
            array[right]=p;
        }
    }
    //pivot 和指針重合點交換
    array[startIndex]=array[left];
    array[left]=pivot;
    return left;
}
  • 單邊循環法

    只從數組的一邊對元素進行遍歷和交換

//快速排序-單邊循環法
public static int partitionSingle(int []array,int startIndex,int endIndex){
    //取第一個位置的元素作爲基準元素,也可以選擇隨機位置
    int pivot = array[startIndex];
    int mark = startIndex;
    for(int i = startIndex+1;i<=endIndex;i++){
        if(array[i]<pivot){
            mark++;
            int p = array[mark];
            array[mark]=array[i];
            array[i]=p;
        }
    }
    array[startIndex]=array[mark];
    array[mark]=pivot;
    return mark;
}

非遞歸快速排序,只需要重寫quikcSort方法爲非遞歸:

//非遞歸
public static void quickSortV2(int[]arr,int startIndex,int endIndex){
    //用一個集合棧來代替遞歸的函數棧
    Stack<Map<String,Integer>> quickSortStack = new Stack<Map<String, Integer>>();
    //整個數列的起止下標,以哈希的形式入棧
    Map rootParam = new HashMap();
    rootParam.put(START_INDEX_KEY,startIndex);
    rootParam.put(END_INDEX_KEY,endIndex);
    quickSortStack.push(rootParam);
    //循環結束條件:棧爲空時
    while (!quickSortStack.isEmpty()){
        //棧頂元素出棧,得到起止下標
        Map<String,Integer> param = quickSortStack.pop();
        //得到基準元素位置
        int pivotIndex = partitionSingle(arr,param.get(START_INDEX_KEY),param.get(END_INDEX_KEY));
        //根據基準元素分爲兩部分,把每一部分的起止下標入棧
        if(param.get(START_INDEX_KEY)<pivotIndex-1){
            Map<String,Integer> leftParam = new HashMap<String, Integer>();
            leftParam.put(START_INDEX_KEY,param.get(START_INDEX_KEY));
            leftParam.put(END_INDEX_KEY,pivotIndex-1);
            quickSortStack.push(leftParam);
        }
        if(pivotIndex+1<param.get(END_INDEX_KEY)){
            Map<String,Integer> rightParam = new HashMap<String, Integer>();
            rightParam.put(START_INDEX_KEY,pivotIndex+1);
            rightParam.put(END_INDEX_KEY,param.get(END_INDEX_KEY));
            quickSortStack.push(rightParam);
        }
    }
}

四、堆排序

算法步驟:

  1. 把無序數組構建成二叉堆。需要從小到達排序,則構建成最大堆;需要從大到小排序,則構建成最小堆。
  2. 循環刪除堆頂元素,替換到二叉堆的末尾,調整堆產生新的堆頂。
//‘下沉’調整
public static void downAdjustV2(int []array,int parentIndex,int length){
    //temp保存父節點值,用於最後的賦值
    int temp = array[parentIndex];
    int 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;
}
//堆排序 (升序)
public static void heapSort(int [] array){
    //1.把無序數組構建成最大堆
    for(int i= (array.length-2)/2;i>=0;i--){
        downAdjustV2(array,i,array.length);
    }
    System.out.println(Arrays.toString(array));
    //2.循環刪除堆頂元素,移動到集合尾部,調整堆產生新的堆頂
    for(int i= array.length-1;i>0;i--){
        //最後一個元素和第一個元素進行交換
        int temp = array[i];
        array[i]=array[0];
        array[0]=temp;
        downAdjustV2(array,0,i);
    }
}

public static void main(String[] args) {
    int[]array = new int[]{
            1,3,2,6,5,7,8,9,10,0
    };
    heapSort(array);
    System.out.println(Arrays.toString(array));
}

五、計數排序

使用數組保存數字出現過的次數,遍歷完待排序的數組後,根據統計數組再生成已排序的數組。

//計數排序
public static int[] countSort(int[]array){
    //得到數列的最大值
    int max = array[0];
    for(int i = 1;i<array.length;i++){
        if(array[i]>max) max=array[i];
    }
    //根據數列最大值確定統計數組的長度
    int[] countArray = new int[max+1];
    //遍歷數列,填充統計數組
    for(int i=0;i<array.length;i++){
        countArray[array[i]]++;
    }
    //遍歷數組,輸出結果
    int index = 0;
    int []sortedArray = new int[array.length];
    for(int i=0;i<countArray.length;i++){
        for(int j = 0;j<countArray[i];j++){
            sortedArray[index++]=i;
        }
    }
    return sortedArray;
}
public static void main(String[] args) {
	int []array = new int[]{
		4,4,6,5,3,2,8,1,7,5,6,0,10
	};
	int[]sortedArray = countSort(array);
	System.out.println(Arrays.toString(sortedArray));
}

優化: 當最大值很大而需要排序的數組長度很小時,可以採用最大值-最小值這一偏移量來初始化統計數組,減小統計數組的長度。

侷限性:

  • 數列最大和最小值差距過大時不適合做計數排序
  • 當數列元素不是整數時也不適合

六、桶排序

每一個桶(bucket)代表一個區間範圍,裏面可以承載一個或這多個元素。

  1. 創建桶

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-KbFDNEJC-1580185021514)(imgs/bucket.png)]

​ 區間跨度 = (最大值 - 最小值)/(桶的數量 - 1)

  1. 遍歷原始數列,把元素對號入座放入各個桶中。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WQbEe423-1580185021515)(imgs/bucket1.png)]

  2. 對每個桶內部的元素分別進行排序

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-PWXSV3v4-1580185021515)(imgs/bucket2.png)]

  3. 遍歷所有的桶,輸出結果

    0.5 0.84 2.18 3.25 4.5

//桶排序
public static double[] bucketSort(double[] array){
    //1.得到數列的最大值和最小值,計算出差值d
    double max = array[0];
    double min = array[0];
    for(int i=1;i<array.length;i++){
        if(array[i]>max) max = array[i];
        if(array[i]<min) min = array[i];
    }
    double d = max-min;
    //2.初始化桶
    int bucketNum = array.length;
    ArrayList<LinkedList<Double>> bucketList = new ArrayList<LinkedList<Double>>(bucketNum);
    for(int i=0;i<bucketNum;i++){
        bucketList.add(new LinkedList<Double>());
    }
    //3.遍歷原始數組,將每個元素放入桶中
    for(int i = 0;i<array.length;i++){
        int num=(int)((array[i]-min)*(bucketNum-1)/d);
        bucketList.get(num).add(array[i]);
    }
    //4.對每個桶內部進行排序
    for(int i =0;i<bucketList.size();i++){
        Collections.sort(bucketList.get(i));
    }
    //5.輸出全部元素
    double[] sortedArray = new double[array.length];
    int index =0;
    for(LinkedList<Double>list:bucketList){
        for(double element:list){
            sortedArray[index]=element;
            index++;
        }
    }
    return sortedArray;
}
public static void main(String[] args) {
	double[]array=new double[]{
		4.12,6.421,0.023,3.0,2.123,8.122,4.12,10.09
	};
	double[] sortedArray = bucketSort(array);
	System.out.println(Arrays.toString(sortedArray));
}
  • 優點:時間複雜度爲O(n),空間複雜度O(n)
  • 缺點:桶排序的性能並非絕對穩定,如果元素的分佈極不均衡,極端情況下,第一個桶中有n-1個元素,最後一個桶有1個元素。此時的時間複雜度將退化爲O(nlogn):

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zRFbRpuS-1580185021516)(imgs/bucket3.png)]

(三)、面試中的算法

一、如何判斷鏈表中有環

類似數學上的追及問題,用兩個指針,一個指針的移動速度是兩個節點,一個的速度是一個節點,從頭結點出發,當兩個節點相遇時,則證明有環。

/**
 * 判斷是否有環
 * @param head 鏈表頭節點
 * @return
 */
public static boolean isCycle(Node head){
    Node p1 = head;
    Node p2 = head;
    while (p2!=null && p2.next!=null){
        p1=p1.next;
        p2 = p2.next.next;
        if(p1==p2){
            return true;
        }
    }
    return false;
}
  • 如果鏈表有環,如何求出環的長度

    當兩個指針首次相遇,則證明有環,讓兩個指針從相遇點繼續循環前進,並統計前進的循環次數,直到兩個指針第二次相遇,此時,統計出來的前進次數就是環長。

    ​ **環長=每一次速度差×前進次數=前進次數 **

  • 如果鏈表有環,如何求出入環點

    當兩個指針首次相遇時,把一個指針放回頭結點位置,另一個指針保持在首次相遇點,兩個指針都是前進一步,他們最終相遇的節點就是入環點

二、最小棧的實現

實現一個棧,該棧帶有出棧(pop)、入棧(push)、取最小元素(getMin)3個方法。要保證這3個方法的時間複雜度都是O(1)。

步驟

  1. 設原有的棧叫作棧A,此時創建一個額外的“備胎”棧B,用於輔助棧A

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LU6mkaF5-1580185021517)(imgs/zhan1.png)]

  1. 當第1個元素進入棧A時,讓新元素也進入棧B。這個唯一的元素是棧A的當前最小值

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9RNd9jmD-1580185021517)(imgs/zhan2.png)]

  1. 之後,每當新元素進入棧A時,比較新元素和棧A當前最小值的大小,如果小於棧A當前最小值,則讓新元素進入棧B,此時棧B的棧頂元素就是棧A當前最小值

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oBcD9YcQ-1580185021518)(imgs/zhan3.png)]

  1. 每當棧A有元素出棧時,如果出棧元素是棧A當前最小值,則讓棧B的棧頂元素也出棧。此時棧B餘下的棧頂元素所指向的,是棧A當中原本第2小的元素,代替剛纔的出棧元素成爲棧A的當前最小值。(備胎轉正。)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tfPnZE4K-1580185021519)(imgs/zhan4.png)]

  1. 當調用getMin方法時,返回棧B的棧頂所存儲的值,這也是棧A的最小值。
    顯然,這個解法中進棧、出棧、取最小值的時間複雜度都是O(1),最壞情況空間複雜度是O(n)。
private Stack<Integer> mainStack = new Stack<Integer>();
private Stack<Integer> minStack = new Stack<Integer>();
/**
 * 入棧操作
 * @param element 入棧的元素
 */
public void push(int element){
    mainStack.push(element);
    //如果輔助棧爲空,或者新元素小於或等於輔助棧棧頂,則將新元素壓入輔助棧
    if(minStack.empty()||element<=minStack.peek()){
        minStack.push(element);
    }
}
//出棧操作
public Integer pop(){
    //如果出棧元素和輔助棧棧頂元素值相等,輔助棧出棧
    if(mainStack.peek().equals(minStack.peek())){
        minStack.pop();
    }
    return mainStack.pop();
}
//獲取棧的最小元素
public int getMin()throws Exception{
    if(mainStack.empty())
        throw new Exception("stack is empty");
    return minStack.peek();
}

三、最大公約數

1. 輾轉相除法

兩個正整數a和b(a>b),它們的最大公約數等於a除以b的餘數c和b之間的最大公約數

O(log(max(a,b)))

//輾轉相除法求最大公約數
public static int getGreatestCommonDivisor(int a,int b){
    int big = a>b ? a : b;
    int small = a<b ? a : b;
    if(big%small==0){
        return small;
    }
    return getGreatestCommonDivisor(big%small,small);
}

2. 更相減損術

兩個正整數a和b(a>b),它們的最大公約數等於a-b的差值c和較小數b的最大公約數

O(max(a,b))

//更相減損術求最大公約數
public static int getGreatestCommonDivisorV2(int a,int b){
    if(a==b) return a;
    int big = a>b ? a : b;
    int small = a<b ? a : b;
    return getGreatestCommonDivisor(big-small,small);
}

3.更相減損術與移位相結合

O(log(max(a,b)))

//更相減損術與移位相結合
public static int gcd(int a,int b){
    if(a==b) return a;
    if((a&1)==0&&(b&1)==0){
        return gcd(a>>1,b>>1)<<1;
    }else if((a&1)==0 && (b&1)!=0){
        return gcd(a>>1,b);
    }else if((a&1)!=0 && (b&1)==0){
        return gcd(a,b>>1);
    }else{
        int big = a>b?a:b;
        int small = a<b?a:b;
        return gcd(big-small,small);
    }
}

四、判斷一個數爲2的整數次冪

對於一個整數n,只需要計算n&(n-1)的結果是不是0,是0則爲2的整數次冪

//判斷一個數爲2的整數次冪
public static boolean isPowerOf2(int num){
    return (num&num-1)==0;
}

五、最大相鄰差

有一個無序整型數組,如何求出該數組排序後的任意兩個相鄰元素的最大差值?要求時間和空間複雜度儘可能低

  1. 利用桶排序的思想,根據原數組的長度n,創建出n個桶,每一個桶代表一個區間範圍。其中第1個桶從原數組的最小值min開始,區間跨度是(max-min)/(n-1)。
  2. 遍歷原數組,把原數組每一個元素插入到對應的桶中,記錄每一個桶的最大和最小值。
  3. 遍歷所有的桶,統計出每一個桶的最大值,和這個桶右側非空桶的最小值的差,數值最大的差即爲原數組排序後的相鄰最大差值。
//最大相鄰差
public static int getMaxSortedDistance(int []array){
    //1.得到最小值和最大值
    int max = array[0];
    int min = array[0];
    for(int i = 1;i<array.length;i++){
        if(array[i]>max) max = array[i];
        if(array[i]<min) min = array[i];
    }
    int d = max-min;
    //如果max 和min相等,則說明所有元素都相等,返回0
    if(d==0){
        return  0;
    }
    //2.初始化桶
    int bucketNum = array.length;
    Bucket[] buckets = new Bucket[bucketNum];
    for(int i = 0;i<bucketNum;i++){
        buckets[i] = new Bucket();
    }
    //3.遍歷原始數組,確定每個桶的最大最小值
    for(int i =0;i<array.length;i++){
        //確定數組元素所屬的下標
        int index = ((array[i]-min)*(bucketNum-1)/d);
        if(buckets[index].min==null||buckets[index].min>array[i]){
            buckets[index].min = array[i];
        }
        if(buckets[index].max==null||buckets[index].max<array[i]){
            buckets[index].max = array[i];
        }
    }
    //4.遍歷桶,找到最大差值
    int leftMax = buckets[0].max;
    int maxdistance = 0;
    for(int i = 1;i<buckets.length;i++){
        if(buckets[i].min==null){
            continue;
        }
        if(buckets[i].min-leftMax>maxdistance){
            maxdistance = buckets[i].min - leftMax;
        }
        leftMax = buckets[i].max;
    }
    return maxdistance;
}
private static class Bucket{
    Integer min;
    Integer max;
}

public static void main(String[] args) {
    int [] array = new int[]{2,6,3,4,5,10,9};
    System.out.println(getMaxSortedDistance(array));
}

六、用棧實現隊列

用兩個堆棧A和B來實現

把新元素都插入A,當第一次出隊時,把A中所有的元素依次彈出,插入到B,這樣順序就反過來了,然後從B出棧,只要B不空,所有的出隊操作從B出,當B爲空時,則再一次把A中所有的元素依次彈出,插入到B。

//用棧實現隊列
private Stack<Integer> stackA = new Stack<Integer>();
private Stack<Integer> stackB = new Stack<Integer>();

//入棧操作
public void enQueue(int element){
    stackA.push(element);
}
//出棧操作
public Integer deQueue(){
    if(stackB.isEmpty()){
        if(stackA.isEmpty()){
            return null;
        }
        transfer();
    }
    return stackB.pop();
}
//棧A轉移到棧B
public void transfer(){
    while (!stackA.isEmpty()){
        stackB.push(stackA.pop());
    }
}

七、尋找全排列的下一個數

給出一個正整數,找出這個正整數所有數字全排列的下一個數。

如果輸入12345,則返回12354。
如果輸入12354,則返回12435。
如果輸入12435,則返回12453。

步驟:

  • 從後向前查看逆序區域,找到逆序區域的前一位,也就是數字置換的邊界。
  • 讓逆序區域的前一位和逆序區域中大於它的最小的數字交換位置
  • 把原來的逆序區域轉爲順序狀態
//尋找全排列的下一個數
public static int[] findNearsetNumber(int[] numbers){
    //1.從後向前查看逆序區域,找到逆序區域的前一位,也就是數字置換的邊界
    int index = findTransferPoint(numbers);
    //如果爲0,說明數組已經逆序,沒有更大的數字
    if(index==0){
        return null;
    }
    //2.把逆序區域的前一位和逆序區域中剛剛大於它的數字交換位置
    int [] numbersCopy = Arrays.copyOf(numbers,numbers.length);
    exchangeHead(numbersCopy,index);
    //3.把原來的逆序區轉爲順序
    reverse(numbersCopy,index);
    return numbersCopy;
}
public static int findTransferPoint(int[] numbers){
    for(int i= numbers.length-1;i>0;i--){
        if(numbers[i]>numbers[i-1]){
            return i;
        }
    }
    return 0;
}
public static void exchangeHead(int[]numbers,int index){
    int head = numbers[index-1];
    for(int i=numbers.length-1;i>0;i--){
        if(numbers[i]>head){
            numbers[index-1]=numbers[i];
            numbers[i]=head;
            break;
        }
    }
}
public static int[] reverse(int []num,int index){
    for(int i=index,j=num.length-1;i<j;i++,j--){
        int temp = num[i];
        num[i]=num[j];
        num[j]=temp;
    }
    return num;
}
private static void outputNumbers(int[]numbers){
    for(int i:numbers){
        System.out.print(i);
    }
    System.out.println();
}

public static void main(String[] args) {
    int[]numbers={1,2,3,4,5};
    for(int i=0;i<10;i++){
        numbers = findNearsetNumber(numbers);
        outputNumbers(numbers);
    }
}

八、刪去k個數字後的最小值

給出一個整數,從該整數中去掉k個數字,要求剩下的數字形成的新整數儘可能小。應該如何選取被去掉的數字?

/**
 * 刪除整數的k個數字,獲得刪除後的最小值
 * @param num 原整數
 * @param k 刪除數量
 * @return
 */
public static String removeKDigits(String num,int k){
    //新整數的長度
    int newLength = num.length()-k;
    //創建一個棧,用於接收所有的數字
    char [] stack = new char[num.length()];
    int top = 0;
    for(int i=0;i<num.length();i++){
        //遍歷當前數字
        char c = num.charAt(i);
        //當棧頂數字大於遍歷到的當前數字時,棧頂數字出棧
        while (top>0&&stack[top-1]>c&&k>0){
            top -=1;
            k-=1;
        }
        //遍歷到的當前數字入棧
        stack[top++]=c;
    }
    //找到棧中第一個非零數字的位置,以此構建新的整數字符串
    int offset = 0;
    while (offset<newLength&&stack[offset]=='0'){
        offset++;
    }
    return offset == newLength? "0":new String(stack,offset,newLength-offset);
}

九、大整數相加

給出兩個很大的整數,要求實現程序求出兩個整數之和。

/**
 * 大整數求和
 * @param bigNumberA
 * @param BigNumberB
 * @return
 */
public static String bigNumberSum(String bigNumberA,String BigNumberB){
    //1.把兩個大整數用數組逆序存儲,數組長度等於較大整數位數+1
    int maxLength = bigNumberA.length()>BigNumberB.length() ? bigNumberA.length():BigNumberB.length();
    int[] arrayA = new int[maxLength+1];
    for(int i = 0;i<bigNumberA.length();i++){
        arrayA[i] = bigNumberA.charAt((bigNumberA.length()-1-i))-'0';
    }
    int [] arrayB = new int[maxLength+1];
    for(int i = 0;i<BigNumberB.length();i++){
        arrayB[i] = BigNumberB.charAt(BigNumberB.length()-i-1)-'0';
    }
    //2.構建result數組,數組長度等於較大整數位數+1
    int []result = new int [maxLength+1];
    //3.遍歷數組,按位相加
    for(int i =0;i<result.length;i++){
        int temp = result[i];
        temp+=arrayA[i];
        temp+=arrayB[i];
        if(temp>=10){
            temp=temp-10;
            result[i+1]=1;
        }
        result[i]=temp;
    }
    //4.把result逆序轉string
    StringBuffer sb = new StringBuffer();
    //是否找到最大整數的最高有效位
    boolean findFirst = false;
    for(int i = result.length-1;i>=0;i--){
        if(!findFirst){
            if(result[i]==0){
                continue;
            }
            findFirst=true;
        }
        sb.append(result[i]);
    }
    return sb.toString();
}

十、金礦問題

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UL4qnZL8-1580185021519)(imgs/huanjin.png)]

 /**
 * 獲得金礦最優收益
 * @param w 工人數量
 * @param p 金礦開採所需要的工人數量
 * @param g 金礦儲量
 * @return
 */
public static int getBestGoldMining(int w,int[] p ,int[] g){
    //創建當前結果
    int[]results = new int[w+1];
    //填充一維數組
    for(int i=1;i<g.length;i++){
        for(int j=w;j>-1;j--){
            if(j>=p[i-1]){
                results[j] = Math.max(results[j],results[j-p[i-1]]+g[i-1]);
            }
        }
    }
    return results[w];
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章