《漫画算法》读书笔记

小灰(小白)的算法之旅

第一章 算法概述

1.1 算法和数据结构
  • 算法(Algorithm):在数学领域用于解决某一类问题的公式和思想。如高斯算法:n×(1+n)÷2,可以用于解决1+2+3+……+(n-1)+n的问题。在计算机领域用于解决特定的运算和逻辑问题。如:运算、查找、排序、最优决策。
  • 数据结构(Data Structure):数据的组织、管理和存储格式。其使用目的是为了高效的访问和修改数据。如:线性结构(数组、链表)、树(二叉树、二叉堆)、图(多对多)、其他数据结构(哈希链表、跳表、位图等)等。
  • 衡量算法的标准有两个时间复杂度空间复杂度
1.2 时间复杂度
  • 基本操作次数T(n):线性T(n) = n;对数T(n) = log(n);常量T(n) = 1;多项式T(n) = n²+n。

  • 渐进时间复杂度:官方定义所存在函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称为O(f(n)),O为算法的渐进时间复杂度,简称为时间复杂度。

  • 时间复杂度推到原则:
    -- 如果运行时间是常数量级,则用常数1表示;
    -- 只保留时间函数中的最高阶项;
    -- 如果最高阶项存在,则省去最高阶项前面的系数。

  • 大O表示法及时长对比:常量O(1) < 对数O(n) =O(logN) < 线性O(n) = n < 多项式O(n) = n²。
    其他算法时间复杂度:O(nlogn)、O(n³)、O(mn)、O(2^n)、O(n!)

1.3 空间复杂度
  • 空间计算公式:S(n) = O(f(n));常量空间O(1)、线性空间O(n)、二维空间O(n²)、递归空间O(n)

递归算法的空间复杂度度同深度成正比。

第二章 数据结构基础

2.1 数组
  • 数组(Array)是有限个相同类型的变量所组成的有序集合,每一个变量被称为元素。
  • 数组在内存中是顺序存储。
  • 操作:读取更新时间复杂度为O(1),插入删除时间复杂度为O(n)。
  • 优势和劣势:数组适合读操作多,写操作少的场景。
  • 数组基本操作相关代码:
public class ArrayBaseOperation {
    //数组定义
    private int[] array = new int[]{3, 1, 2, 5, 4, 9, 7, 2};
    //记录元素个数
    private int size = array.length;

    private void demo() {
        //读取
        System.out.println("原始数据:");
        output();
        //更新
        array[2] = 10;
        //插入
        insert(8, 2);
        System.out.println("插入后的数据:");
        output();

        //删除
        delete(5);
        System.out.println("删除后的数据:");
        output();

    }

    private void output(){
        for (int i = 0; i < size; i++) {
            System.out.print(array[i] + " ");
        }
        //System.out.println(Arrays.toString(array));
    }

    /**
     * 删除操作
     * @param index 删除的位置
     * @return
     */
    public int delete(int index) {
        checkIndexBounds(index);
        int deletedElement = array[index];
        for (int i = index; i < size; i++) {
            array[i] = array[i + 1];
        }
        size--;
        return deletedElement;
    }

    /**
     * 插入操作
     * @param element 插入的元素
     * @param index   插入的位置
     */
    public void insert(int element, int index) {
        checkIndexBounds(index);
        if (size >= array.length) {
            //扩容
            resize();
        }
        for (int i = size - 1; i >= index; i--) {
            array[i + 1] = array[i];
        }
        array[index] = element;
        size++;
    }

    private void checkIndexBounds(int index) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("超出数组实际元素范围!");
        }
    }

    /**
     * 扩容
     */
    private void resize() {
        int[] arrayNew = new int[array.length * 2];
        System.arraycopy(array, 0, arrayNew, 0, array.length);
        array = arrayNew;
    }

    /**
     * @return 获取元素个数
     */
    public int getSize() {
        return size;
    }

    public static void main(String[] args) {
        ArrayBaseOperation operation = new ArrayBaseOperation();
        operation.demo();
    }
}

输出结果:

> Task :lib:ArrayBaseOperation.main()
原始数据:
3 1 2 5 4 9 7 2 
插入后的数据:
3 1 8 10 5 4 9 7 2 
删除后的数据:
3 1 8 10 5 9 7 2 
BUILD SUCCESSFUL in 0s
2.2 链表
  • 链表(Linked List)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。
  • 链表在内存中是随机存储。
  • 链表分单向链表双向链表
  • 操作:查找时间复杂度是O(n),更新插入删除时间复杂度是O(1)(不考虑查找过程)。
  • 优势和劣势:与数组相反,适合读操作少,写操作多的场景。
  • 链表相关操作代码:
public class LinkedListBaseOperation {
    private Node head;//头节点
    private Node last;//尾节点
    private int size;//链表长度

    public void demo() {
        //插入数据
        insert(3, 0);
//        output();
        insert(5, 1);
//        output();
        insert(7, 2);
//        output();
        insert(9, 0);
//        output();
        insert(4, 3);
//        output();
        insert(2, 3);
//        output();
        insert(1, 1);
        System.out.println("插入数据后:");
        //读取数据
        output();
        //删除数据
        remove(0);
        remove(3);
//        remove(4);
        System.out.println("删除数据后:");
        output();
    }

    /**
     * 数据输出
     */
    public void output() {
        Node temp = head;
        while (temp != null) {
            System.out.print(temp.data + " ");
            temp = temp.next;
        }
        System.out.println();
    }

    /**
     * 数据删除
     * @param index 指定删除节点位置
     */
    public Node remove(int index) {
        checkIndexBounds(index);
        Node removedNode;
        if (index == 0) {//删除头节点
            removedNode = head;
            head = head.next;
        } else if (index == size - 1) {//删除尾节点
            Node prevNode = get(index-1);
            removedNode = prevNode.next;
            prevNode.next = null;
            last = prevNode;
        } else {
            Node prevNode = get(index-1);
            Node nextNode = prevNode.next.next;
            removedNode = prevNode.next;
            prevNode.next = nextNode;
        }
        size--;
        return removedNode;
    }

    /**
     * @param data  插入的数据
     * @param index 插入的位置
     */
    public void insert(int data, int index) {
        checkIndexBounds(index);
        Node insertedNode = new Node(data);
        if (size == 0) {
            head = insertedNode;
            last = insertedNode;
        } else if (index == 0) {
            insertedNode.next = head;
            head = insertedNode;
        } else {
            Node prevNode = get(index - 1);//查找数据
            Node nextNode = prevNode.next;
            insertedNode.next = nextNode;
            prevNode.next = insertedNode;
        }
        size++;
    }

    /**
     * 数据查找
     * @param index 节点位置
     * @return 获取指定位置节点
     */
    private Node get(int index) {
        checkIndexBounds(index);
        Node temp = head;
        for (int i = 0; i < index; i++) {
            temp = temp.next;
        }
        return temp;
    }

    private void checkIndexBounds(int index) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException("超出链表节点范围!");
        }
    }

    /**
     * 单向链表
     */
    private static class Node {
        int data;
        Node next;

        Node(int data) {
            this.data = data;
        }
    }

    /**
     * 双向链表
     */
    private static class NodeTW {
        int data;
        NodeTW next;
        NodeTW prev;

        NodeTW(int data) {
            this.data = data;
        }
    }

    public static void main(String[] args) {
        LinkedListBaseOperation operation = new LinkedListBaseOperation();
        operation.demo();
    }
}

输出结果:

> Task :lib:LinkedListBaseOperation.main()
插入数据后:
9 1 3 5 2 4 7 
删除数据后:
1 3 5 4 7 
BUILD SUCCESSFUL in 0s
2.3 栈和队列
  • 是一种线性结构,它就像入口和出口是同一个口的圆桶容器,元素只能先进后出(First In Last Out,简称FILO)。最先进入的叫栈底,最后进入的叫栈顶
  • 栈的基本操作:入栈(push)新元素进入栈中成为新的栈顶;出栈(pop)栈顶出栈,前一个元素成为新的栈顶。时间复杂度都是O(1)。
  • 队列是一种线性结构,它就像出口和入口是不同口的单行隧道,元素只能先入先出(First In First Out,简称FIFO)。出口端是队头,入口端是队尾。
  • 操作:入队(enqueue)是把新的元素放入队列中并且成为新的队尾。出队(dequeue)就是把元素移除队列,并且后一个元素成为新的队头。
  • 应用:栈可以用来做历史回溯,例如浏览器的回溯上一个页面。队列可以用来做历史顺序重演,例如公平锁队列等待,或者是网络爬虫模拟网站数据抓取。

实现方式:栈和队列都可以用数组或链表实现,但是队列中使用数组实现,称为循环队列容量比数组长度小1。
循环队列关键算法:{(队头/队尾下标+1)%数组长度 }计算新队头/队尾下标,如果与队头下标相等,则队列已满。
双端队列:队头队尾都可以做入队或出队操作。
优先队列:非线性结构,按优先规则出队,如二叉堆。

  • 循环队列相关代码
/**
 * 循环队列
 */
public class QueueBaseOperation {
    private int[] array;
    private int front;
    private int rear;
    private int size;

    public QueueBaseOperation(int capacity) {
        array = new int[capacity];
    }

    public void demo() {
        try {
            enQueue(1); enQueue(3); enQueue(5); enQueue(7); enQueue(9); enQueue(7);
            System.out.println("入队后:");
            output();
            deQueue();deQueue();deQueue();
            System.out.println("出队后:");
            output();
            System.out.println("再入队:");
            enQueue(1); enQueue(3); enQueue(5); enQueue(7);
            output();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void output() {
        for (int i = 0; i < size; i++) {
            System.out.print(array[(front + i) % array.length] + " ");
        }
        System.out.println();
    }

    /**
     * 入队
     *
     * @param element 入队元素
     * @throws Exception 队列满了,抛出异常
     */
    public void enQueue(int element) throws Exception {
        if ((rear + 1) % array.length == front) {
            throw new Exception("队列已满!");
        }
        array[rear] = element;
        rear = (rear + 1) % array.length;
        size++;
    }

    /**
     * 出队
     *
     * @return 出队元素
     * @throws Exception 队列空了,抛出异常
     */
    public int deQueue() throws Exception {
        if (rear == front) {
            throw new Exception("队列已空!");
        }
        int deQueueElement = array[front];
        front = (front + 1) % array.length;
        size--;
        return deQueueElement;
    }

    public static void main(String[] args) {
        QueueBaseOperation operation = new QueueBaseOperation(10);
        operation.demo();
    }
}

输出结果:

> Task :lib:QueueBaseOperation.main()
入队后:
1 3 5 7 9 7 
出队后:
7 9 7 
7 9 7 1 3 5 7 9 7 
BUILD SUCCESSFUL in 0s
2.4 散列表
  • 散列表又叫哈希表(hash table),存储键(key)值(value)的映射关系的集合(数组+链表/红黑树)。对于某一个key,散列表可以在接近O(1)的时间进行读写操作。散列表通过哈希函数实现Key和数组下标的转换,通过开放寻址法和链表法来解决哈希冲突。

哈希冲突:哈希算法在计算数组下标的时候可能会产生相同的下标,这就是所谓的冲突。
开放寻址法:产生哈希冲突,就向后移位寻找空位。如Java中ThreadLocal使用的就是此方法。
链表法:数组中每个元素可以作为链表头节点,冲突的元素向链表冲插入即可。
扩容(resize):当元素的数量>=负载因子×数组长度时,需要进行扩充哈希表容量,首先是生成新的数组,然后对原始数据中的元素重新Hash(rehash)计算,放入新的哈希表中。

第三章 树和二叉树

3.1 树
  • 树(tree)是n(n>=0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点:1.有且有一个特定的称为根的节点。2.当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每个集合又是一个树,并称为根的子树。
  • 二叉树:是树的特殊形式,每个节点最多又两个子节点。
  • 满二叉树:一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上。
  • 完全二叉树:对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树的所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。
  • 链式存储结构:数据变量、左孩子指针、右孩子指针。
  • 数组存储:父节点parent、左孩子下标2×parent+1、有孩子下标2×parent+2。
  • 查找应用:二叉查找树(binary search tree)
    --如果左树不为空,则左子树所有节点的值均小于根节点的值。
    --如果右树不为空,则右子树所有节点的值均大于根节点的值。
    --左、右子树也都是二叉查找树。
    对于节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度一样。(类似二分查找
  • 维持相对顺序应用:二叉排序树(binary sort tree)
    二叉排序树即是二叉查找树,在插入元素时同样要满足左、右子树特点,但是有时候会导致元素分布不均衡的情况,需要通过自平衡解决,如红黑树AVL树树堆等。
3.2 二叉树的遍历
  • 深度优先遍历
    --前序遍历:根节点、左子树、右子树。
    --中序遍历:左子树、根节点、右子树。
    --后序遍历:左子树、右子树、根节点。
    实现方式用递归
  • 广度优先遍历
    --层序遍历:从根节点到叶子节点逐层遍历。
    实现方式用队列
  • 二叉树遍历相关代码:
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

public class BinaryTreeTraversal {

    public void demo() {
        LinkedList inputList = new LinkedList<>(Arrays.asList(3, 2, 9, null, null, 10, null, null, 8, null, 4));
        TreeNode node = createBinaryTree(inputList);
        System.out.println("前序遍历:");
        preOrderTraversal(node);
        System.out.println();
        System.out.println("中序遍历:");
        inOrderTraversal(node);
        System.out.println();
        System.out.println("后序遍历:");
        postOrderTraversal(node);
        System.out.println();
        System.out.println("层序遍历:");
        levelOrderTraversal(node);
    }

    /**
     * 构建二叉树
     * @param inputList 数据列表
     * @return 根节点
     */
    public TreeNode createBinaryTree(LinkedList inputList) {
        TreeNode treeNode = null;
        if (inputList != null && !inputList.isEmpty()) {
            Integer data = inputList.removeFirst();
            if (data != null) {
                treeNode = new TreeNode(data);
                treeNode.leftChild = createBinaryTree(inputList);
                treeNode.rightChild = createBinaryTree(inputList);
            }
        }
        return treeNode;
    }

    /**
     * 前序遍历
     * @param node 根节点
     */
    public void preOrderTraversal(TreeNode node) {
        if (node == null) return;
        System.out.print(node.data + " ");
        preOrderTraversal(node.leftChild);
        preOrderTraversal(node.rightChild);
    }

    /**
     * 中序遍历
     * @param node 根节点
     */
    public void inOrderTraversal(TreeNode node) {
        if (node == null) return;
        inOrderTraversal(node.leftChild);
        System.out.print(node.data + " ");
        inOrderTraversal(node.rightChild);
    }

    /**
     * 后序遍历
     * @param node 根节点
     */
    public void postOrderTraversal(TreeNode node) {
        if (node == null) return;
        postOrderTraversal(node.leftChild);
        postOrderTraversal(node.rightChild);
        System.out.print(node.data + " ");
    }

    /**
     * 层序遍历
     * @param node 根节点
     */
    public void levelOrderTraversal(TreeNode node) {
        Queue queue = new LinkedList<>();
        queue.offer(node);
        while (!queue.isEmpty()) {
            TreeNode treeNode = queue.poll();
            System.out.print(treeNode.data + " ");
            if (treeNode.leftChild != null) {
                queue.offer(treeNode.leftChild);
            }
            if (treeNode.rightChild != null) {
                queue.offer(treeNode.rightChild);
            }
        }
    }

    private static class TreeNode {
        int data;
        TreeNode leftChild;
        TreeNode rightChild;

        TreeNode(int data) {
            this.data = data;
        }
    }

    public static void main(String[] args) {
        BinaryTreeTraversal traversal = new BinaryTreeTraversal();
        traversal.demo();
    }
}

输出内容:

> Task :lib:BinaryTreeTraversal.main()
前序遍历:
3 2 9 10 8 4 
中序遍历:
9 2 10 3 8 4 
后序遍历:
9 10 2 4 8 3 
层序遍历:
3 2 8 9 10 4 
BUILD SUCCESSFUL in 0s

树形图:

                3
             /      \
           2          8
         /   \       /   \
        9    10    null    4
      /   \ /   \ 
  null null null null

构建二叉树的构建递归过程比较难理解,先左后右,null值控制左右节点递归回溯(如果列表中没有null值,则所有数据为左树子节点),数据列表顺序与前序遍历顺序一致

3.3 二叉堆
  • 最大堆(大顶堆):任何一个父节点的值,都大于或等于它左、右孩子节点的值。
  • 最小堆(小顶堆):任何一个父节点的值,都小于或等于它左、右孩子节点的值。
  • 插入节点:插入位置是完全二叉树最后一个位置,然后与父节点对比做“上浮”操作。
  • 删除节点:删除堆顶,将完全二叉树最后一个节点移位到堆顶,与左右孩子中最小或最大节点对比做“下沉”操作。
  • 构建二叉堆:将一个无序的二叉树调整为一个二叉堆,本质是让所有非叶子节点依次下沉。(与左右孩子中最小或最大节点对比)

插入删除时间复杂度是:O(logn);构建二叉堆时间复杂度是:O(n)。

  • 二叉堆操作相关代码:
import java.util.Arrays;

public class BinaryTreeHeap {

    public void demo() {
        System.out.println("上浮前:");
        int[] array = new int[]{1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
        System.out.println(Arrays.toString(array));
        upAdjust(array);
        System.out.println("上浮后:");
        System.out.println(Arrays.toString(array));
        System.out.println("无序数组:");
        array = new int[]{7, 1, 3, 10, 5, 2, 8, 9, 6};
        System.out.println(Arrays.toString(array));
        buildHeap(array);
        System.out.println("构建成二叉堆后:");
        System.out.println(Arrays.toString(array));
    }

    /**
     * 上浮(最小堆)
     * @param array 待调整数组
     */
    public void upAdjust(int[] array) {
        int childIndex = array.length - 1;
        int parentIndex = (childIndex - 1) / 2;
        int temp = array[childIndex];
        while (childIndex > 0 && temp < array[parentIndex]) {
            array[childIndex] = array[parentIndex];
            childIndex = parentIndex;
            parentIndex = (parentIndex - 1) / 2;
        }
        array[childIndex] = temp;
    }

    /**
     * 下浮(最小堆)
     * @param array 待调整数组
     * @param parentIndex 父节点位置
     */
    public void downAdjust(int[] array, int parentIndex) {
        int temp = array[parentIndex];
        int childIndex = parentIndex * 2 + 1;
        int length = array.length;
        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 = childIndex * 2 + 1;
        }
        array[parentIndex] = temp;
    }

    /**
     * 将无序数组构建二叉堆(最小堆)
     * @param array 无序的数组
     */
    public void buildHeap(int[] array) {
        for (int i = (array.length - 2) / 2; i >= 0; i--) {
            downAdjust(array, i);
        }
    }

    public static void main(String[] args) {
        BinaryTreeHeap heap = new BinaryTreeHeap();
        heap.demo();
    }
}

输出结果:

> Task :lib:BinaryTreeHeap.main()
上浮前:
[1, 3, 2, 6, 5, 7, 8, 9, 10, 0]
上浮后:
[0, 1, 2, 6, 3, 7, 8, 9, 10, 5]
无序数组:
[7, 1, 3, 10, 5, 2, 8, 9, 6]
构建成二叉堆后:
[1, 5, 2, 6, 7, 3, 8, 9, 10]
BUILD SUCCESSFUL in 0s

二叉堆为顺序存储(数组),childLeft = parent2+1,childRight = parent2+2。
上面的demo都是以最小堆特点进行构建和排序。

3.4 优先队列
  • 队列的特点是先进先出(FIFO),而优先队列,则是基于二叉堆的特点所实现的一种顺序队列。优先队列可以根据对比元素的大小值,进而判断元素的优先出队顺序,于元素的入队顺序无关。
  • 优先队列的代码实现基本与二叉堆无异,只是在入队时需要进行动态扩容,出队时需要删除堆顶。

第四章 排序算法

4.1 引言
时间复杂度 排序算法名称
O(n²) 冒泡排序、选择排序、插入排序、希尔排序
O(nlogn) 快速排序、归并排序、堆排序
线性 计数排序、桶排序、基数排序

希尔排序比较特殊,它的性能略优于O(n²),但又略低于O(nlogn)

  • 稳定排序不稳定排序:稳定排序能保证相同元素在排序前和排序后的相对顺序,不稳定排序则不能。
4.2 冒泡排序
  • 冒泡排序(dubble sort),是一种基础的交换排序。
    升序:把相邻的两个元素进行比较,当一个元素大于右侧相邻元素时,交换他们的位置;当一个元素小于或等于相邻元素时,位置不变。
    冒泡排序属于稳定排序,时间复杂度为O(n²)。
  • 鸡尾酒排序:由冒泡排序演化而来,如果说普通冒泡排序是单向排序,鸡尾酒则是双向排序。适用于大部分元素有序的情况。
  • 代码实现:
package com.power.dapengeducation.lib;

import java.util.Arrays;
import java.util.Random;

public class BubbleSort {

    private int[] arr = {7, 8, 9, 1, 2, 3, 6, 5, 4};

    public void demo() {
//        int len = 100000;
//        arr = new int[len];
//        Random random = new Random();
//        for (int i = 0; i array[j + 1]) {
                    tmp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tmp;
                    isSorted = false;
                }
            }
            if (isSorted) break;
            isSorted = true;
            //右半边左移
            for (int j = array.length - i - 1; j > i; j--) {
                if (array[j] < array[j - 1]) {
                    tmp = array[j];
                    array[j] = array[j - 1];
                    array[j - 1] = tmp;
                    isSorted = false;
                }
            }
            if (isSorted) break;
        }
    }

    /**
     * 二次优化的冒泡排序(升序)
     *
     * @param array 待排序数组
     */
    public static void optimalSortAsc(int[] array) {
        int sortBorder = array.length - 1;//记录无序边界
        int sortBorderTemp = 0;
        for (int i = 0; i < array.length; i++) {
            boolean isSorted = true;//有序标记
            for (int j = 0; j < sortBorder; j++) {
                if (array[j] > array[j + 1]) {
                    int temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    isSorted = false;
                    sortBorderTemp = j;
                }
            }
            sortBorder = sortBorderTemp;
            if (isSorted) {
                break;
            }
        }
    }

    /**
     * 无优化的冒泡排序(升序)
     *
     * @param array 待排序数组
     */
    private static void baseSortAsc(int array[]) {
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length - 1; j++) {
                if (array[j] > array[j + 1]) {
                    int tmp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tmp;
                }
            }
        }
    }

    public static void main(String[] args) {
        BubbleSort sort = new BubbleSort();
        sort.demo();
    }
}

输出结果:

> Task :lib:BubbleSort.main()
排序结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]
BUILD SUCCESSFUL in 0s
4.3 快速排序
  • 快速排序也属于交换排序。快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到另一边,从而把数列拆解成两个部分。这种思路叫分治法。
  • 平均时间复杂度O(nlogn)。
  • 选择基准元素:随机。
  • 元素交换:双边循环法单边循环法
  • 相关代码实现:
import java.util.Arrays;

public class QuickSort {

    public void demo() {
        int[] arr = {4, 4, 6, 5, 3, 2, 8, 1};
        System.out.println("排序前:" + Arrays.toString(arr));
//        quickSortBilateral(arr, 0, arr.length - 1);
        quickSortUnilateral(arr, 0, arr.length - 1);
        System.out.println("排序后:" + Arrays.toString(arr));
    }

    /**
     * 单边循环法快速排序
     *
     * @param arr        待排序数组
     * @param startIndex 起始位置
     * @param endIndex   结束位置
     */
    public void quickSortUnilateral(int[] arr, int startIndex, int endIndex) {
        if (startIndex >= endIndex) return;
        int pivotIndex = partitionUnilateral(arr, startIndex, endIndex);
        quickSortUnilateral(arr, startIndex, pivotIndex - 1);
        quickSortUnilateral(arr, pivotIndex + 1, endIndex);
    }

    /**
     * 单边循环法分治
     *
     * @param arr        待排序数组
     * @param startIndex 起始位置
     * @param endIndex   结束位置
     * @return 基准位置
     */
    private int partitionUnilateral(int[] arr, int startIndex, int endIndex) {
        int pivot = arr[startIndex];
        int mark = startIndex;
        for (int i = startIndex + 1; i <= endIndex; i++) {
            if (arr[i] < pivot) {
                mark++;
                int temp = arr[i];
                arr[i] = arr[mark];
                arr[mark] = temp;
            }
        }
        arr[startIndex] = arr[mark];
        arr[mark] = pivot;
        return mark;
    }

    /**
     * 双边循环快速排序
     *
     * @param arr        待排序数组
     * @param startIndex 起始下标
     * @param endIndex   结束下标
     */
    public void quickSortBilateral(int[] arr, int startIndex, int endIndex) {
        if (startIndex >= endIndex) return;
        int pivotIndex = partitionBilateral(arr, startIndex, endIndex);
        quickSortBilateral(arr, startIndex, pivotIndex - 1);
        quickSortBilateral(arr, pivotIndex + 1, endIndex);
    }

    /**
     * 双边循环分治
     *
     * @param arr        待交换数组
     * @param startIndex 起始下标
     * @param endIndex   结束下标
     * @return
     */
    public int partitionBilateral(int[] arr, int startIndex, int endIndex) {
        int pivot = arr[startIndex];//也可以随机选择基准元素
        int left = startIndex;
        int right = endIndex;

        while (left != right) {
            while (right > left && arr[right] > pivot) {
                right--;
            }
            while (left < right && arr[left] <= pivot) {
                left++;
            }
            if (arr[left] > arr[right]) {
                int temp = arr[left];
                arr[left] = arr[right];
                arr[right] = temp;
            }
        }
        arr[startIndex] = arr[left];
        arr[left] = pivot;
        return left;
    }

    public static void main(String[] args) {
        QuickSort sort = new QuickSort();
        sort.demo();
    }
}

输出结果:

> Task :lib:QuickSort.main()
排序前:[4, 4, 6, 5, 3, 2, 8, 1]
排序后:[1, 2, 3, 4, 4, 5, 6, 8]
BUILD SUCCESSFUL in 0s

以上排序代码是用递归方式实现,也可以用栈模拟递归实现排序算法,代码略微复杂,此处省略。

4.4 堆排序
  • 堆排序同3.3节 二叉堆的上浮、下浮操作。
  • 快速排序对比:平均时间复杂度都是O(nlogn),并且都是不稳定排序。快速排序最坏时间复杂度是O(n²),而堆排序最坏时间复杂度稳定在O(nlogn)。空间复杂度:快速排序是O(logn),堆排序是O(1).
4.5 计数排序和桶排序
  • 计数排序:对原始数据不做变更,先生成一个统计数组,统计数组长度为待排序数组的最大值(max value),利用统计数组下标与待排序数组元素对比,相同就在统计数组对应下标的元素+1,最后按照统计数组中记录的个数依次输出下标值(0跳过)的顺序,既是排序后的数据。

计数排序优化(稳定排序):统计数组长度为(max - min +1)。统计数组中元素值与前面所有元素值相加(变形),得出最终排序位置,然后对待排序数组进行倒序遍历,在统计数组查出对应元素的值并-1,便会得出待排序数组元素排序后的下标值。
计数排序适用于整数排序,不适用于最大值与最小值差距过大或者元素不是整数。

  • 优化后的计数排序代码:
import java.util.Arrays;

public class CountSort {

    public void demo() {
        int[] arr = {95, 94, 91, 98, 99, 90, 99, 93, 91, 92};
        System.out.println(Arrays.toString(countSort(arr)));
    }

    public int[] countSort(int[] arr) {
        int max = arr[0];
        int min = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (max < arr[i]) {
                max = arr[i];
            }
            if (min > arr[i]) {
                min = arr[i];
            }
        }
        int d = max - min;
        //创建统计数组并统计元素个数
        int[] countArray = new int[d + 1];
        for (int i = 0; i < arr.length; i++) {
            countArray[arr[i] - min]++;
        }
        //变形处理
        for (int i = 1; i < countArray.length; i++) {
            countArray[i] += countArray[i - 1];
        }
        //根据统计数组得出排序后数组
        int[] sortedArray = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            sortedArray[countArray[arr[i] - min] - 1] = arr[i];
            countArray[arr[i] - min]--;
        }
        return sortedArray;
    }

    public static void main(String[] args) {
        CountSort countSort = new CountSort();
        countSort.demo();
    }
}

输出结果:

:lib:CountSort.main()
[90, 91, 91, 92, 93, 94, 95, 98, 99, 99]
BUILD SUCCESSFUL in 2s
  • 桶排序:桶排序需要创建若干个桶协助排序可以与元素个数相同或其他,每个桶代表一个区间范围区间跨度 = (max - min) /( 同数量-1),里面可以装载一个或多个元素,桶按照区间值大小顺序排列,再分别对每个桶中的元素进行排序排序算法自行选择

桶排序能解决计数排序的缺点,对整型或浮点型数据都支持,需要注意的是桶的数量控制。

  • 桶排序相关代码:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;

public class BucketSort {

    public void demo() {
        double[] arr = {4.12, 6.421, 0.0023, 3.0, 2.123, 8.122, 4.12, 10.09};
        System.out.println(Arrays.toString(bucketSort(arr)));
    }

    public double[] bucketSort(double[] array) {
        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];
            }
        }
        //初始化桶
        int bucketNum = array.length;
        double d = max - min;
        ArrayList> bucketList = new ArrayList<>(bucketNum);
        for (int i = 0; i < bucketNum; i++) {
            bucketList.add(new LinkedList());
        }
        //向桶中填充元素
        for (int i = 0; i < array.length; i++) {
            //找出桶的位置(有点难理解)
            int index = (int) ((array[i] - min) * (bucketNum - 1) / d);
            //放入桶中
            bucketList.get(index).add(array[i]);
        }
        //排序
        for (int i = 0; i < bucketList.size(); i++) {
            Collections.sort(bucketList.get(i));
        }
        //输出
        double[] sortedArray = new double[array.length];
        int index = 0;
        for (LinkedList list : bucketList) {
            for (double element : list) {
                sortedArray[index] = element;
                index++;
            }
        }
        return sortedArray;
    }

    public static void main(String[] args) {
        BucketSort sort = new BucketSort();
        sort.demo();
    }
}

输出结果:

> Task :lib:BucketSort.main()
[0.0023, 2.123, 3.0, 4.12, 4.12, 6.421, 8.122, 10.09]
BUILD SUCCESSFUL in 0s
4.6 小结
排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 是否稳定排序
冒泡排序 O(n²) O(n²) O(1) 稳定
鸡尾酒排序 O(n²) O(n²) O(1) 稳定
快速排序 O(nlogn) O(n²) O(logn) 不稳定
堆排序 O(nlogn) O(nlogn) O(1) 不稳定
计数排序 O(n+m) O(n+m) O(m) 稳定
桶排序 O(n) O(nlogn) O(n) 稳定

第五章 面试中的算法

5.1 判断链表有环及环长度
  • 双指针追及法:一个指p1针做单步遍历p1.next,另一个指针p2做双步遍历p2.next.next,如果有环,则一定会出现重合p1==p2环长度D头节点到入环节点距离,S1入环节点到相遇节点距离,S2相遇节点到入环节点距离。p2走两步,所以行走距离D+S1+S2+S1p1走一步,所以行走距离是D+S1,重合时p2走的距离是p1的两倍,所以2(D+S1) = D+2S1+S2 => D=S2。当第一次重合时,挪动一个指针到head,且两个指针都做单步遍历,当再次相遇则是入环节点,由此可以得出D/S2的长度,所以环长度=链表长度-D
  • 相关代码:
import com.ieugene.algorithmdemo.LinkedListBaseOperation.Node;

public class LinkedCycle {
    private int nodeSize = 0;//demo

    public void demo() {
        Node node1 = new Node(5);
        nodeSize++;
        Node node2 = new Node(3);
        nodeSize++;
        Node node3 = new Node(7);
        nodeSize++;
        Node node4 = new Node(2);
        nodeSize++;
        Node node5 = new Node(6);
        nodeSize++;
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;
        node5.next = node2;
        System.out.println("链表环长度:" + cycleLength(node1));
        System.out.println("链表是否有环:" + hasCycle(node1));
    }

    public int cycleLength(Node head) {
        Node head1 = head;
        Node head2 = head;
        while (head1.next != null && head2.next.next != null) {
            head1 = head1.next;
            head2 = head2.next.next;
            if (head1 == head2) {
                head2 = head;
                break;
            }
        }
        int len = 0;
        while (head1.next != null && head1 != head2) {
            head1 = head1.next;
            head2 = head2.next;
            len++;
        }
        return nodeSize - len;
    }

    /**
     * 判断链表中是否有环
     *
     * @param head 链表头节点
     * @return 有环返回true,否则是false
     */
    public boolean hasCycle(Node head) {
        Node head1 = head;
        Node head2 = head;
        while (head1.next != null && head2.next.next != null) {
            head1 = head1.next;
            head2 = head2.next.next;
            if (head1 == head2) return true;
        }
        return false;
    }

    public static void main(String[] args) {
        LinkedCycle cycle = new LinkedCycle();
        cycle.demo();
    }
}

输出结果:

链表环长度:4
链表是否有环:true
Process finished with exit code 0
5.2 最小栈
  • 题要求:保证出栈、入栈、取最小值时间复杂度都是O(1)。
  • 思路:用备用栈存储最小值。
  • 相关代码:
import java.util.Stack;

public class MinStack {
    private Stack mainStack = new Stack<>();
    private Stack minStack = new Stack<>();

    public void demo() {
        push(4);
        push(9);
        push(7);
        push(3);
        push(8);
        push(5);
        System.out.println("最小值:" + getMin());
        pop();
        pop();
        pop();
        System.out.println("三次出栈后最小值:" + getMin());
    }

    public void push(int element) {
        mainStack.push(element);
        if (minStack.isEmpty() || minStack.peek() >= element) {
            minStack.push(element);
        }
    }

    public int pop() {
        int element = mainStack.pop();
        if (!minStack.isEmpty() && minStack.peek().equals(element)) {
            minStack.pop();
        }
        return element;
    }

    public int getMin() {
        return minStack.peek();
    }

    public static void main(String[] args) {
        MinStack stack = new MinStack();
        stack.demo();
    }
}

输出结果:

最小值:3
三次出栈后最小值:4
Process finished with exit code 0
5.4 求最大公约数
  • 如果数a能被数b整除,a就叫做b的倍数,b就叫做a的约数最大公约数指的是两个或多个整数共同的最大约数,也叫最大公因数。
  • 思路:
    --辗转相除法(欧几里得算法),两个正整数a和b(a>b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。
    --更相减损法(更相减损术),是出自《九章算术》的一种求最大公约数的算法,它原本是为约分而设计的,但它适用于任何需要求最大公约数的场合。
    -- 对比:辗转相除法(欧几里得算法)是基于取模运算,当两个数较大时,性能较差;更相减损法(更相减损术)是基于减法运算得出结果,当两个数大小差距较大时,计算性能也会被降低。
    --结论:两个算法结合应用,并在更相减损法(更相减损术)基础上使用移位运算

《九章算术》是中国古代的数学专著,其中的“更相减损术”可以用来求两个数的最大公约数,即“可半者半之,不可半者,副置分母、子之数,以少减多,更相减损,求其等也。以等数约之。”
-翻译成现代语言如下:
第一步:任意给定两个正整数;判断它们是否都是偶数。若是,则用2约简;若不是则执行第二步。
第二步:以较大的数减较小的数,接着把所得的差与较小的数比较,并以大数减小数。继续这个操作,直到所得的减数和差相等为止。
则第一步中约掉的若干个2与第二步中等数的乘积就是所求的最大公约数。
其中所说的“等数”,就是最大公约数。求“等数”的办法是“更相减损”法。所以更相减损法也叫等值算法
移位运算:位运算相关知识
当a和b均为偶数时,gcd(a,b) = 2 × gcd(a/2, b/2) = gcd(a>>1, b>>1)<<1。
当a为偶数,b为奇数时,gcd(a,b) = gcd(a>>1, b)。
当a为奇数,b为偶数时,gcd(a,b) = gcd(a, b>>1)。
当a和b均为奇数时,做一次更相减损gcd(a,b) = gcd(a-b, b)。

  • 相关代码
public class GreatestCommonDivisor {

    public void demo() {
        System.out.println(getGCD(25, 5));
        System.out.println(getGCD(100, 80));
        System.out.println(getGCD(27, 14));

        //System.out.println(getGCD2(25, 5));
        //System.out.println(getGCD2(100, 80));
        //System.out.println(getGCD2(27, 14));

        //System.out.println(getGCD3(25, 5));
        //System.out.println(getGCD3(100, 80));
        //System.out.println(getGCD3(27, 14));
    }

    public int getGCD(int a, int b) {
        if (a == b) return a;
        if ((a & 1) == 0 && (b & 1) == 0) {
            return getGCD(a >> 1, b >> 1) << 1;
        } else if ((a & 1) == 0 && (b & 1) != 0) {
            return getGCD(a >> 1, b);
        } else if ((a & 1) != 0 && (b & 1) == 0) {
            return getGCD(a, b >> 1);
        } else {
            int big = Math.max(a, b);
            int small = Math.min(a, b);
            return getGCD(big - small, small);
        }
    }

    /**
     * 辗转相除法
     *
     * @param a 正整数
     * @param b 正整数
     * @return 最大公约数
     */
    public int getGCD2(int a, int b) {
        int big = Math.max(a, b);
        int small = Math.min(a, b);
        if (big % small == 0) return small;
        return getGCD2(big % small, small);
    }

    /**
     * 更相减损术
     *
     * @param a 正整数
     * @param b 正整数
     * @return 最大公约数
     */
    public int getGCD3(int a, int b) {
        if (a == b) return a;
        int big = Math.max(a, b);
        int small = Math.min(a, b);
        return getGCD3(big - small, small);
    }

    public static void main(String[] args) {
        GreatestCommonDivisor divisor = new GreatestCommonDivisor();
        divisor.demo();
    }
}

输出结果:

5
20
1
Process finished with exit code 0
5.5 求无序数列排序后相邻元素最大差
  • 思路:利用桶排序原理,将无序数列放置到各自桶中,同时得出每个桶中的最大值和最小值,然后遍历每个桶,用前面的桶最大值与后面桶最小值做差值计算,遍历完成边能得出最大差值。
5.6 用栈实现队列
  • 思路:用两个栈存储队列元素,栈A模拟入栈操作,栈B模拟出栈操作。出栈操作之前如果栈B空了,需要将栈A中的元素导入栈B中。
  • 相关代码:
import java.util.Stack;

public class StackSimulateQueue {
    Stack pushStack = new Stack<>();
    Stack popStack = new Stack<>();

    public void demo() {
        enqueue(1);
        enqueue(2);
        enqueue(3);
        System.out.println("第一次出队:" + dequeue());
        System.out.println("第二次出队:" + dequeue());
        enqueue(4);
        System.out.println("第三次出队:" + dequeue());
        System.out.println("第四次出队:" + dequeue());
    }

    //入队
    public void enqueue(int element) {
        pushStack.push(element);
    }

    //出队
    public Integer dequeue() {
        if (popStack.isEmpty()) {
            if (pushStack.isEmpty()) {
                return null;
            }
            transfer();
        }
        return popStack.pop();
    }

    private void transfer() {
        while (!pushStack.isEmpty()) {
            popStack.push(pushStack.pop());
        }
    }

    public static void main(String[] args) {
        StackSimulateQueue queue = new StackSimulateQueue();
        queue.demo();
    }
}

输出结果:

第一次出队:1
第二次出队:2
第三次出队:3
第四次出队:4
Process finished with exit code 0
5.7 寻找全排列的下一个数
  • 题意:整数中全部数组重新排列,找出一个仅大于原数的数列。例如给出数字12345,全排列的下一个数是12354。
  • 思路:字典序算法
    --从后向前查看逆序区,找到逆序区域的前一位,也就是数字置换的边界。
    --从逆序区域的前一位和逆序区与中大于它的最小的数字交换位置。
    --把原来的逆序区域转为顺序状态。
  • 相关代码:
import java.util.Arrays;

public class FindNearestNumber {

    public void demo() {
//        int[] numbers = {1, 2, 3, 4, 5};
//        int[] numbers = {1, 2, 3, 5, 4};
        int[] numbers = {1, 2, 3, 5, 4, 7, 9};
        System.out.println(Arrays.toString(findNearestNumber(numbers)));
    }

    public int[] findNearestNumber(int[] numbers) {
        //从后向前查找逆序边界
        int index = findTransferPoint(numbers);
        //复制并入参,避免直接修改入参
        int[] numbersCopy = Arrays.copyOf(numbers, numbers.length);
        //如果边界值是0,说明当前是最小值
        if (index == 0) return reverseLast(numbersCopy);
        //把逆序区域的前一位和逆序区域中刚刚大于它的数字交换位置
        exchangeHead(numbersCopy, index);
        //把原来逆序区转为顺序
        reverse(numbersCopy, index);
        return numbersCopy;
    }

    private int[] reverseLast(int[] numbers) {
        int len = numbers.length;
        int temp = numbers[len - 1];
        numbers[len - 1] = numbers[len - 2];
        numbers[len - 2] = temp;
        return numbers;
    }

    private int findTransferPoint(int[] numbers) {
        for (int i = numbers.length - 1; i > 0; i--) {
            if (numbers[i - 1] > numbers[i]) {
                return i - 1;
            }
        }
        return 0;
    }

    private int[] exchangeHead(int[] number, int index) {
        int head = number[index - 1];
        int nearestIndex = 0;
        int min = Integer.MAX_VALUE;
        for (int i = number.length - 1; i >= index; i--) {
            if (head < number[i] && (number[i] - head) < min) {
                nearestIndex = i;
                min = number[i] - head;
            }
        }
        number[index - 1] = number[nearestIndex];
        number[nearestIndex] = head;
        return number;
    }

    private int[] reverse(int[] number, int index) {
        for (int i = index; i < number.length; i++) {
            boolean isSorted = true;
            for (int j = index; j < number.length - 1; j++) {
                if (number[j] > number[j + 1]) {
                    int temp = number[j];
                    number[j] = number[j + 1];
                    number[j + 1] = temp;
                    isSorted = false;
                }
            }
            if (isSorted) break;
        }
        return number;
    }

    public static void main(String[] args) {
        FindNearestNumber nearestNumber = new FindNearestNumber();
        nearestNumber.demo();
    }
}

输出结果:

[1, 2, 4, 3, 5, 7, 9]
Process finished with exit code 0
5.8 删去k个数字后的最小值
  • 题意:从一个整数中删去k个数字,使得新整数的值尽量小。例如:1593212,删除后是1212。
  • 思路:贪心算法,依次求得局部最优解,最终得到全局最优解
    --局部最优解:完全正序数列12345的最大值是它的逆序数列54321,反过来依然成立,由此可以得出逆序数列边界删除,便是局部最优解。
  • 相关代码:
public class RemoveDigits {

    public void demo() {
        System.out.println(removeDigits("1593212", 3));
        System.out.println(removeDigits("30200", 1));
        System.out.println(removeDigits("10", 2));
        System.out.println(removeDigits("541270936", 3));
    }

    public String removeDigits(String num, int 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) {
                k -= 1;
                top -= 1;
            }
            stack[top++] = c;
        }
        //找出栈底开始都是0的位置
        int offset = 0;
        int newLen = num.length() - k;
        while (offset < newLen && stack[offset] == '0') {
            offset++;
        }
        return offset == newLen ? "0" : new String(stack, offset, newLen - offset);
    }

    public static void main(String[] args) {
        new RemoveDigits().demo();
    }
}

输出结果:

1212   
200 
0
120936   
Process finished with exit code 0
5.9 大整数相加
  • 题意:两个长度超过long类型的大整数相加,求结果。
  • 思路:拆分数据,拆分长度为可以直接计算的长度即可,例如int类型数据是10位,为了防止溢出,可以拆分到9位即可。
  • 相关代码
import java.util.Arrays;

public class BigNumberSum {

    final int MAX_LENGTH = 9;
    final int CARRY_VALUE = 1000000000;

    public void demo() {
        System.out.println(bigNumberSum("987654321987654321987", "222234567891"));
    }

    public String bigNumberSum(String bigNumberA, String bigNumberB) {
        //拆分大数字
        int[] arrayA = splitBigNumber(bigNumberA);
        int[] arrayB = splitBigNumber(bigNumberB);
        //创建与最大数字等长的数组,用于接收计算结果
        int[] arraySum = new int[Math.max(arrayA.length, arrayB.length)];
        System.out.println("array A: " + Arrays.toString(arrayA));
        System.out.println("array B: " + Arrays.toString(arrayB));
        int minLen = Math.min(arrayA.length, arrayB.length);
        //判断是否需要进位
        boolean isCarry = false;
        for (int i = 0; i < minLen; i++) {
            arraySum[i] = arrayA[i] + arrayB[i];
            if (isCarry) {
                arraySum[i] += 1;
                isCarry = false;
            }
            if (arraySum[i] >= CARRY_VALUE) {
                arraySum[i] -= CARRY_VALUE;
                isCarry = true;
            }
        }
        for (int i = minLen; i < arraySum.length; i++) {
            if (arrayA.length > arrayB.length) {
                arraySum[i] = arrayA[i];
            } else {
                arraySum[i] = arrayB[i];
            }
            if (isCarry) {
                arraySum[i] += 1;
                isCarry = false;
            }
        }
        StringBuilder builder = new StringBuilder();
        for (int i = arraySum.length - 1; i >= 0; i--) {
            if (arraySum[i] == 0) {
                builder.append("000000000");
            } else {
                builder.append(arraySum[i]);
            }

        }
        return builder.toString();
    }

    private int[] splitBigNumber(String number) {
        int length = getArrayLength(number);
        int[] array = new int[length];
        int end = number.length();
        int start;
        for (int i = 0; i < length; i++) {
            start = Math.max(end - MAX_LENGTH, 0);
            array[i] = Integer.parseInt(number.substring(start, end));
            end = start;
        }
        return array;
    }

    private int getArrayLength(String number) {
        int len = 1;
        if (number.length() > MAX_LENGTH) {
            len = number.length() / MAX_LENGTH;
            if (number.length() % MAX_LENGTH != 0)
                len++;
        }
        return len;
    }

    public static void main(String[] args) {
        new BigNumberSum().demo();
    }
}

输出结果:

array A: [654321987, 654321987, 987]
array B: [234567891, 222]
987654322209888889878
Process finished with exit code 0
5.10 求解金矿问题
  • 题:有五个金矿,每个金矿含金量不同,每个金矿要求的开采开采人数不同,不允许开采一部分,要么都开采,要不都不采。
  • 思路:这个问题属于动态规划类问题,类似与著名的“背包问题”,记得《算法图解》中就是用的背包问题进行讲解的。
    --动态规划要点:确定全局最优解和最优子结构之间的关系,以及问题的边界。
    --动态规划核心:自底向上求解
    --这个关系用数学公式表达:状态转移方程式
//n为金矿数量
//w为工人数量
//g[]为金矿含金量数组
//p[]金矿开采所需人数数组
F(n,w) = 0 (n=0或w=0)  //边界情况
F(n,w) = F(n-1,w)(n>=1,w=1,w>=p[n-1]) //常规情况,具有两种最优子结构(当前金矿开采或不开采)
  • 相关代码
public class BestGoldMining {

    public void demo() {
        int w = 10;
        int[] p = {5, 5, 3, 4, 3};
        int[] g = {400, 500, 200, 300, 350};
        System.out.println("最优收益:" + getBestGoldMining(w, p, g));
    }

    /**
     * 开采金矿最优收益
     *
     * @param w 总人数
     * @param p 金矿所需人数
     * @param g 金矿含量
     * @return
     */
    public 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];
    }

    public static void main(String[] args) {
        new BestGoldMining().demo();
    }
}

输出结果:

最优收益:900
Process finished with exit code 0
5.11 寻找缺失的整数
  • 题1:一个无序数组里有99个不重复的正整数,范围是1~100,唯独缺少范围内的一个整数,找出这个缺失的整数。
  • 解1:1~100的和减去无序数组所有元素的和,差值便是缺失的整数。
  • 题2:一个无序整数数组里有若干个正整数,范围是1~100,其中99个数出现偶数次,只有一个整数出现奇数次,找出出现奇数次的整数。
  • 解2:利用异或运算(XOR),同位得0,不同位得1。将所有元素依次做异或运算,出现偶数次的元素相互抵消,最后只剩下出现奇数次的元素。
  • 题3:如果题2中有两个出现奇数次的元素,请找出。
  • 解3:利用分治法。假设两个数是A和B,如果所有元素依次做异或运算,就相当于A和B做异或运算,根据异或运算规则可以得出,A和B的二进制一定会有一个不同位,一个是1,一个是0,然后根据这个二进制数的特点进行分类,倒数第二位为1的归为一类,倒数第二位为0的归为一类。这样问题就回归到问题2,按照问题2的解法便可以找出A和B。
  • 相关代码:
import java.util.Arrays;

public class FindLostNumber {

    public void demo() {
        int[] array = {4, 1, 2, 2, 5, 1, 4, 3};
        System.out.println("两个数是:" + Arrays.toString(findLostNumber(array)));
    }

    public int[] findLostNumber(int[] array) {
        //存储结果
        int[] reslut = new int[2];
        //第一次做整体异或运算
        int xorResult = 0;
        for (int i = 0; i < array.length; i++) {
            xorResult ^= array[i];
        }
        if (xorResult == 0) return null;//等于0说明输入数据不符合题要求
        //确定两个整数的不同位,以此来做分组
        int separator = 1;
        while (0 == (xorResult & separator)) {
            separator <<= 1;
        }
        //第二次分组,进行异或运算
        for (int i = 0; i < array.length; i++) {
            if (0 == (array[i] & separator)) {
                reslut[0] ^= array[i];
            } else {
                reslut[1] ^= array[i];
            }
        }
        return reslut;
    }

    public static void main(String[] args) {
        new FindLostNumber().demo();
    }
}

输出结果:

两个数是:[5, 3]
Process finished with exit code 0

第六章 算法的实际应用

6.1 Bitmap的巧用
  • Bitmap算法又称位图算法。低内存占用,高性能位运算是它的优势。缺点是最大最小值差距不能太大。
  • 场景:用户标签分类查找
  • Bitmap算法参考:JDK中BitSet
  • Bitmap读写操作相关代码:
public class Bitmap {
    //每一个word是一个long类型元素,对应一个64位二进制数据
    private long[] words;
    //Bitmap的位数大小
    private int size;

    public Bitmap(int size) {
        this.size = size;
        this.words = new long[(getWordIndex(size - 1)) + 1];
    }

    /**
     * 判单Bitmap某一位状态
     *
     * @param bitIndex 位图的第bitIndex位(bitIndex=0表示Bitmap左数第一位)
     * @return
     */
    public boolean getBit(int bitIndex) {
        checkBounds(bitIndex);
        int wordIndex = getWordIndex(bitIndex);
        return (words[wordIndex] & (1L << bitIndex)) != 0;
    }

    private void checkBounds(int index) {
        if (index < 0 || index > this.size - 1) {
            throw new IndexOutOfBoundsException("超过Bitmap的有效范围");
        }
    }

    /**
     * 把Bitmap的某一位设置为true
     *
     * @param bitIndex 位图的第bitIndex位(bitIndex=0表示Bitmap左数第一位)
     */
    public void setBit(int bitIndex) {
        checkBounds(bitIndex);
        int wordIndex = getWordIndex(bitIndex);
        words[wordIndex] |= (1L << bitIndex);
    }

    /**
     * 定位Bitmap某一位所对应的word
     *
     * @param bitIndex 位图的第bitIndex位(bitIndex=0表示Bitmap左数第一位)
     * @return
     */
    private int getWordIndex(int bitIndex) {
        return bitIndex >> 6;
    }

    public static void main(String[] args) {
        Bitmap bitmap = new Bitmap(128);
        bitmap.setBit(126);
        bitmap.setBit(75);
        System.out.println(bitmap.getBit(126));
        System.out.println(bitmap.getBit(78));
    }
}

输出结果:

true
false
Process finished with exit code 0
6.2 LRU算法应用
  • LRU全称Least Recently Used,最近最少使用。
  • 使用场景:长期不被使用,使用机率不大的数据,当内存达到一定阀值,就会清理电最少被使用的数据。
  • 原理:内部使用哈希链表数据结构,使得哈希表中无序的数据存在前驱、后继的排列顺序关系。每次数据的读写都会重新排列链表结构。Java中LinkedHashMap对哈希链表有了很好的实现。
  • 相关代码:
import com.ieugene.algorithmdemo.LinkedListBaseOperation.NodeTW;
import java.util.HashMap;

public class LRUCache {
    private NodeTW head;
    private NodeTW end;
    //存储上限
    private final int limit;
    private final HashMap hashMap;

    public LRUCache(int limit) {
        this.limit = limit;
        hashMap = new HashMap<>();
    }

    public Integer get(String key) {
        NodeTW node = hashMap.get(key);
        if (node == null) return null;
        refreshNode(node);
        return node.data;
    }

    public void put(String key, int value) {
        NodeTW node = hashMap.get(key);
        if (node == null) {
            //key不存在,插入数据
            if (hashMap.size() >= limit) {
                String oldKey = removeNode(head);
                hashMap.remove(oldKey);
            }
            node = new NodeTW(value);
            node.key = key;
            addNode(node);
            hashMap.put(key, node);
        } else {
            //key存在,刷新数据
            node.data = value;
            refreshNode(node);
        }
    }

    public void remove(String key) {
        NodeTW node = hashMap.get(key);
        removeNode(node);
        hashMap.remove(key);
    }

    /**
     * 刷新被访问的节点
     *
     * @param node 被访问的节点
     */
    private void refreshNode(NodeTW node) {
        //尾节点不需要重排
        if (node == end) {
            return;
        }
        //先删除
        removeNode(node);
        //在重新添加
        addNode(node);
    }

    /**
     * 删除节点
     *
     * @param node 被删除的节点
     * @return 被删除的节点的key
     */
    private String removeNode(NodeTW node) {
        //只有一个节点
        if (node == head && node == end) {
            head = null;
            end = null;
        } else if (node == end) {
            end = end.prev;
            end.next = null;
        } else if (node == head) {
            head = head.next;
            head.prev = null;
        } else {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
        return node.key;
    }

    /**
     * 插入节点
     *
     * @param node 被插入的节点
     */
    private void addNode(NodeTW node) {
        if (end != null) {
            end.next = node;
            node.prev = end;
            node.next = null;
        }
        end = node;
        if (head == null) {
            head = node;
        }
    }

    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(5);
        lruCache.put("001", 1);
        lruCache.put("002", 1);
        lruCache.put("003", 1);
        lruCache.put("004", 1);
        lruCache.put("005", 1);
        lruCache.get("002");
        lruCache.put("004", 2);
        lruCache.put("006", 6);
        System.out.println(lruCache.get("001"));
        System.out.println(lruCache.get("006"));
    }
}

输出结果:

null
6
Process finished with exit code 0
6.3 A星寻路算法
  • A * search algorithm
  • 原理:
    --两个集合:OpenList:可到达的格子、CloseList:已到达的格子。
    --一个公式:F = G + H
    --步骤:1.从起点开始放入OpenList中,计算OpenList中F值最小的格子作为下一步即将到达的格子,然后从OpenList移除,添加到CloseList,表示已经检查过。2.找出上下左右所有可能到达的格子,看他们是否在两个列表中,如果不在则加入到OpenList中,并计算响应的G、H、F值,并把当前的格子作为他们的“父节点”。以此类推,重复迭代这个过程,如果OpenList中存在目标格子,每一个父节点和目标格子所经历的路径即是最短路径。

每一个格子有三个属性,左下G:从起点走到当前格子的成本,也就是已经花费了多少步;右下H:在不考虑障碍的情况下,从当前格子走到目标格子的距离,也就是离目标还有多远;左上F:G和H的综合评估,也就是从起点到达目标格子的总步数。
以估值高低来决定搜索优先次序的方法被称为启发式搜索

  • 相关代码:
import java.util.ArrayList;
import java.util.List;

public class AStartSearch {
    private static final int[][] MAZE = {
            {0, 0, 0, 0, 0, 0, 0},
            {0, 0, 0, 1, 0, 0, 0},
            {0, 0, 0, 1, 0, 0, 0},
            {0, 0, 0, 1, 0, 0, 0},
            {0, 0, 0, 0, 0, 0, 0}
    };

    public Grid aStartSearch(Grid start, Grid end) {
        ArrayList openList = new ArrayList<>();
        ArrayList closeList = new ArrayList<>();
        openList.add(start);
        while (openList.size() > 0) {
            Grid currentGrid = findMinGrid(openList);
            openList.remove(currentGrid);
            closeList.add(currentGrid);
            List neighbors = findNeighbors(currentGrid, openList, closeList);
            for (Grid grid : neighbors) {
                if (!openList.contains(grid)) {
                    grid.initGrid(currentGrid, end);
                    openList.add(grid);
                }
            }
            for (Grid grid : openList) {
                if ((grid.x == end.x) && grid.y == end.y) {
                    return grid;
                }
            }
        }
        return null;
    }

    private Grid findMinGrid(ArrayList openList) {
        Grid tempGrid = openList.get(0);
        for (Grid grid : openList) {
            if (grid.f < tempGrid.f) {
                tempGrid = grid;
            }
        }
        return tempGrid;
    }

    private ArrayList findNeighbors(Grid grid, List openList, List closeList) {
        ArrayList gridList = new ArrayList<>();
        if (isValidGrid(grid.x, grid.y - 1, openList, closeList)) {
            gridList.add(new Grid(grid.x, grid.y - 1));
        }
        if (isValidGrid(grid.x, grid.y + 1, openList, closeList)) {
            gridList.add(new Grid(grid.x, grid.y + 1));
        }
        if (isValidGrid(grid.x - 1, grid.y, openList, closeList)) {
            gridList.add(new Grid(grid.x - 1, grid.y));
        }
        if (isValidGrid(grid.x + 1, grid.y, openList, closeList)) {
            gridList.add(new Grid(grid.x + 1, grid.y));
        }
        return gridList;
    }

    private boolean isValidGrid(int x, int y, List openList, List closeList) {
        if (x < 0 || x >= MAZE.length || y < 0 || y >= MAZE[0].length) {
            return false;
        }
        //障碍物
        if (MAZE[x][y] == 1) {
            return false;
        }
        if (containGrid(openList, x, y)) {
            return false;
        }
        if (containGrid(closeList, x, y)) {
            return false;
        }
        return true;
    }

    private boolean containGrid(List grids, int x, int y) {
        for (Grid n : grids) {
            if ((n.x == x) && (n.y == y)) {
                return true;
            }
        }
        return false;
    }

    private static class Grid {
        int x, y;
        int f, g, h;
        Grid parent;

        public Grid(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public void initGrid(Grid parent, Grid end) {
            this.parent = parent;
            if (parent != null) {
                this.g = parent.g + 1;
            } else {
                this.g = 1;
            }
            this.h = Math.abs(this.x - end.x) + Math.abs(this.y - end.y);
            this.f = this.g + this.h;
        }
    }

    public static void main(String[] args) {
        Grid startGrid = new Grid(2, 1);
        Grid endGrid = new Grid(2, 5);
        AStartSearch startSearch = new AStartSearch();
        Grid result = startSearch.aStartSearch(startGrid, endGrid);
        ArrayList path = new ArrayList<>();
        while (result != null) {
            path.add(new Grid(result.x, result.y));
            result = result.parent;
        }
        for (int i = 0; i < MAZE.length; i++) {
            for (int j = 0; j < MAZE[0].length; j++) {
                if (startSearch.containGrid(path, i, j)) {
                    System.out.print("*, ");
                } else {
                    System.out.print(MAZE[i][j] + ", ");
                }
            }
            System.out.println();
        }
    }
}

输出结果:

0, 0, *, *, *, *, 0, 
0, 0, *, 1, 0, *, 0, 
0, *, *, 1, 0, *, 0, 
0, 0, 0, 1, 0, 0, 0, 
0, 0, 0, 0, 0, 0, 0, 
Process finished with exit code 0
6.4 红包算法
  • 场景:随机拆分红包,数额差距尽量缩小。
  • 算法:
    --二倍均值法,公式:每次抢到的金额 = 随机区间[0.01, m/n * 2 - 0.01]元,m为剩余金额,n为剩余人数。
    --线段切割法,n个人抢,随机计算出n-1个切割点,随机范围区间是[1, m - 1],注意切割点重复和算法复杂度。
  • 二倍均值法代码:
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class DivideRedPackage {

    public void demo() {
        List amountList = divideRedPackage(1000, 10);
        for (Integer amount : amountList) {
            System.out.println("抢到的金额:" + new BigDecimal(amount).divide(new BigDecimal(100)));
        }
    }

    public List divideRedPackage(Integer totalAmount, Integer totalPeopleNum) {
        List amountList = new ArrayList<>();
        Integer restAmount = totalAmount;
        Integer restPeopleNum = totalPeopleNum;
        Random random = new Random();
        for (int i = 0; i < totalPeopleNum - 1; i++) {
            int amount = random.nextInt(restAmount / restPeopleNum * 2 - 2) + 1;
            restAmount -= amount;
            restPeopleNum--;
            amountList.add(amount);
        }
        amountList.add(restAmount);
        return amountList;
    }

    public static void main(String[] args) {
        new DivideRedPackage().demo();
    }
}

输出结果:

抢到的金额:0.98
抢到的金额:1.5
抢到的金额:0.58
抢到的金额:0.52
抢到的金额:0.15
抢到的金额:1.55
抢到的金额:0.69
抢到的金额:2.09
抢到的金额:0.68
抢到的金额:1.26
Process finished with exit code 0

总结

总体来说还是通俗易懂的,但还是要吐槽一下勘误略多,缺乏严谨,还有个别地方不统一,例如最后面红包问题描述,单位就不统一,一会儿是元一会儿是分,对读者来说不是很友好。

你可能感兴趣的:(《漫画算法》读书笔记)