漫画算法-学习笔记

漫画算法–笔记

数据结构补充知识

散列表

散列表也叫作哈希表(hash table),这种数据结构提供了键(Key)和值(Value)的映射关系。

计算key到index的转化

通过哈希函数,我们可以把字符串或其他类型的Key,转化成数组的下标index。
i n d e x = H a s h C o d e ( K e y ) % A r r a y . l e n g t h index = HashCode (Key) \% Array.length index=HashCode(Key)%Array.length

散列表读写操作
  1. 写操作
    • 通过计算得到index,然后将value进行存入,但是随着数量的增加,计算出相同的index,就会出现 哈希冲突

    • 冲突解决方式

      • 开放寻址法

        当一个Key通过哈希函数获得对应的数组下标已被占用时,我们可以“另谋高就”,寻找下一个空档位置。

      • 链表法

        HashMap数组的每一个元素不仅是一个Entry对象,还是一个链表的头节点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中即可。

  2. 读操作

    第1步,通过哈希函数,把Key转化成数组下标2。

    第2步,找到数组下标2所对应的元素,如果这个元素的Key是002936,那么就找到了;如果这个Key不是002936也没关系,由于数组的每个元素都与一个链表对应,我们可以顺着链表慢慢往下找,看看能否找到与Key相匹配的节点。

  3. 扩容

    当经过多次元素插入,散列表达到一定饱和度时,Key映射位置发生冲突的概率会逐渐提高。

    Capacity,即HashMap的当前长度
    LoadFactor,即HashMap的负载因子,默认值为0.75f

    衡量HashMap需要进行扩容的条件如下。
    H a s h M a p . S i z e > = C a p a c i t y × L o a d F a c t o r HashMap.Size >= Capacity×LoadFactor HashMap.Size>=Capacity×LoadFactor

    • 步骤

      1.扩容,创建一个新的Entry空数组,长度是原数组的2倍。
      2.重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组中。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V4XecqpM-1667801087716)(https://cdn.jsdelivr.net/gh/hututu-tech/IMG-gongfeng@main/2022/03/10/62295abcba928.jpg)]

      经过扩容,原本拥挤的散列表重新变得稀疏,原有的Entry也重新得到了尽可能均匀的分配。

补充知识

树(tree)是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点。

  1. 有且仅有一个特定的称为根的节点。

  2. 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。

二叉树遍历

归类

  1. 深度优先遍历(前序遍历、中序遍历、后序遍历)。
  2. 广度优先遍历(层序遍历)。

解析

  1. 前序遍历

输出顺序是根节点、左子树、右子树。

  1. 中序遍历

输出顺序是左子树、根节点、右子树

  1. 后序遍历。

输出顺序是左子树、右子树、根节点

深度优先

这3种遍历方式的区别,仅仅是输出的执行位置不同:前序遍历的输出在前,中序遍历的输出在中间,后序遍历的输出在最后。

public static void inOrderTraveral(TreeNode node){
if(node == null){
return;
}
//System.out.println(node.data);//前序遍历
inOrderTraveral(node.leftChild);
//System.out.println(node.data);中序遍历
inOrderTraveral(node.rightChild);
//System.out.println(node.data);//后序遍历
}

非递归方式

使用栈的进出进行模拟

public static void preOrderTraveralWithStack(TreeNode root){
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;
 }
 }
 }
广度优先

使用队列进行访问

对每一层将其子节点加入队列当中,然后将当前节点推出队列

  1. 层序遍历。

二叉堆

二叉堆本质上是一种完全二叉树,它分为两个类型。

  1. 最大堆。
  2. 最小堆。
二叉堆的自我调整

对于二叉堆,有如下几种操作。

  1. 插入节点。
  2. 删除节点。
  3. 构建二叉堆。
  1. 插入节点

    当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。

    “上浮”

    通过与上父节点的大小比较判断是否需要进行交换取值

  2. 删除节点

    二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点。

    这时,为了继续维持完全二叉树的结构,我们把堆的最后一个节点临时补到原本堆顶的位置。

    “下沉”

    从堆顶开始进行与子节点的判断大小

    • 两者都满足情况
      • 最小堆–交换最小的
      • 最大堆–交换最大的
构建二叉堆

构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质就是让所有非叶子节点依次“下沉”。

代码实现

二叉堆虽然是一个完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。换句话说,二叉堆的所有节点都存储在数组中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-90Wi57yv-1667801087716)(https://cdn.jsdelivr.net/gh/hututu-tech/IMG-gongfeng@main/2022/03/10/622965474f0e1.jpg)]

假设父节点的下标是parent,那么它的左孩子下标就是2×parent+1;右孩子下标就是2×parent+2

优先队列
  1. 最大堆的堆顶是整个堆中的最大元素。
  2. 最小堆的堆顶是整个堆中的最小元素。
    因此,可以用最大堆来实现最大优先队列,这样的话,每一次入队操作就是堆的插入操作,每一次出队操作就是删除堆顶节点。

排序算法

主流的排序算法

  1. 时间复杂度为
    O ( n 2 ) O(n^2) O(n2)
    的排序算法
    冒泡排序
    选择排序
    插入排序
    希尔排序(希尔排序比较特殊,它的性能略优于O(n 2 ),但又比不上O(nlogn),姑且把它归入本类)

  2. 时间复杂度为
    O ( n l o g n ) O(nlogn) O(nlogn)
    的排序算法
    快速排序
    归并排序
    堆排序

  3. 时间复杂度为线性的排序算法
    计数排序
    桶排序
    基数排序

根据稳定性分类:

排序后,相同值的块内顺序是否保持不变

  1. 稳定排序–不变
  2. 不稳定排序

冒泡算法

public static void sort(int array[]) {
    for (int i = 0; i < array.length - 1; i++) {
        for (int j = 0; j < array.length - i - 1; j++) {
            if (array[j] > array[j + 1]) {
                int temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
            }
        }
    }
}

时间复杂度:n^2

优化
  1. 如果没有交换的操作–意味着已经排好序,后面的循环就没有必要了

  2. 登记有序区,不进行排序

    public static void sort(int array[]) {
        int sortBorder = array.length - 1;//
        int lastIndex = 0;
        for (int i = 0; i < array.length - 1; 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;
                    lastIndex = j;
                    isSorted = false;
                }
            }
            sortBorder = lastIndex;
            if (isSorted) break;
        }
    }
    

鸡尾酒排序

鸡尾酒排序的元素比较和交换过程是双向的。

先从左到右,再从右到左

每一轮进行判断是否有进行排序,循环的次数相当原来冒泡的一半

public static void sort(int array[]) {
    int tmp = 0;
    for (int i = 0; i < array.length / 2; i++) {
        boolean isSorted = true;
        for (int j = i; j < array.length - i - 1; j++) {
            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 (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;
        }
    }
}

快速排序

分治法

快速排序则在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分。

基准元素的选择

随机选择一个元素作为基准元素,并且让基准元素和数列首元素交换位置。

元素的交换
  1. 双边循环法。
  2. 单边循环法。

双边循环

  1. 设置基准元素

  2. 设置左右指针

  3. 单轮

    1. 从右指针开始,遍历到小于基准的数停下
    2. 到左指针,遍历到大于基准的数停下
    3. 交换左右指针
    4. 当左右指针重合,设置基准到重合点
    5. 以基准点分割成两部分,进行递归
    6. 当数组长度==1,进行返回。
        public static void quickSort(int arr[], int startIndex, int endIndex) {
            //判断返回条件
            if (startIndex >= endIndex) return;
            //进行操作
            int pivotIndex = partition(arr, startIndex, endIndex);
    //        更新参数状态进行递归使用
            quickSort(arr, startIndex, pivotIndex - 1);
            quickSort(arr, pivotIndex + 1, endIndex);
        }
    
        private static int partition(int[] arr, int startIndex, int endIndex) {
            int left = startIndex, right = endIndex, pviot = arr[startIndex];
            while (left < right) {
                while (arr[right] > pviot) right--;
                while (arr[left] < pviot) left++;
                if (left < right) {
                    int temp = arr[left];
                    arr[left] = arr[right];
                    arr[right] = temp;
                }
            }
            arr[startIndex] = arr[left];
            arr[left] = pviot;
            return left;
        }
    

单边循环

  1. 设置基准位置,mark指针标记小于基准部分边界
  2. 对元素进行遍历
    • 当发现小于基准时,mark指针右移,且与当前元素进行交换
    • 最后mark成为基准
private static int partition(int[] arr, int startIndex, int endIndex) {
    int mark = startIndex, pviot = arr[startIndex];
    for (int i = startIndex + 1; i <= endIndex; i++) {
        //判断当前元素是否比基准小
        if (arr[i] < pviot) {
            mark++;
            int temp = arr[mark];
            arr[mark] = arr[i];
            arr[i] = temp;
        }
    }
    //交换基准位置
    arr[startIndex] = arr[mark];
    arr[mark] = pviot;
    return mark;
}

堆排序

使用最大堆或者最小堆的**“上升”以及“下沉”**进行删除的操作实现数组大小的排序过程

步骤

  1. 把无序数组构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆。(从下往上看)

  2. 循环删除堆顶元素,替换到二叉堆的末尾调整堆产生新的堆顶

计数排序

根据数的范围创建一个统计型数列,

数列的下标指的是数值,值对应这个值出现的次数。

public static int[] countSort(int[] array) {
    int max = 0;
    for (int i : array) {
        if (i > max) max = i;
    }
    int[] tempArray = new int[max + 1];
    for (int item : array) {
        tempArray[item]++;
    }
    int index=0;
    int[] res = new int[array.length];
    for (int i = 0; i < tempArray.length; i++) {
        for (int j = 0; j < tempArray[i]; j++) {
            res[index++] = i;
        }
    }
    return res;
}
优化

最小值不一定是0,而且如果是90–100,索引数组的前部分就会被浪费

因此应该通过最大最小值进行数组的创建,并且使用偏移值进行下标的对应计算

public static int[] countSort(int[] array) {
    int max = 0;
    int min = 0;
    for (int i : array) {
        if (i > max) max = i;
        else min = i;
    }
    int[] tempArray = new int[max - min + 1];
    for (int item : array) {
        tempArray[item-min]++;
    }
    int index = 0;
    int[] res = new int[array.length];
    for (int i = 0; i < tempArray.length; i++) {
        for (int j = 0; j < tempArray[i]; j++) {
            res[index++] = i+min;
        }
    }
    return res;
}

以上的问题是一种不稳定排序,因为同一分数下需要进行进行数据对象的区分,此时就需要进行优化

进行了统计之后,对统计数组进行变形,从第二个元素开始,数值=当前数值+前面的和

  • 关键
    • 进行遍历插入的时候,需要从原数组后面向前进行访问
    • (同一分数下,存在多个)在进行数据的访问之后,需要对该位置的数值进行减一
局限
  1. 最值区间过大
  2. 元素不是整数

桶排序

将数组范围进行均等平分,分成n-1个区间,将数据遍历装载到每个区间内,遍历对每个区间进行内部的排序。

区间跨度 = (最大值-最小值)/ (桶的数量 - 1)

面试中的算法

链表相关
链表是否有环
  1. 使用hashSet进行对经过路径的保存

  2. 使用快慢指针,因为有环的话,快的必定会再遇到慢的。

    • 注意

      在遍历的过程当中,针对快指针是否有值来判断该链是否有尾部。

public boolean hasCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head;
    int length = 0;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;

        if (fast == slow) {
            slow = slow.next;
            //第一次进入相遇点,开始从这里开始进行步长的判断
            fast = fast.next.next;
            length++;
            while (fast != slow) {
                slow = slow.next;
                fast = fast.next.next;
                length++;
            }
            //再次相遇,进行推出
        }

    }
    return false;
}

拓展

  1. 环的长度

    • 当第一次相遇后开始计算,到达第二次相遇停止

    • 环的长度=速度差*前进次数

  2. 入环点的计算

最小栈的实现
  • 通过备用栈进行最小值的记录

    class MinStack {
    
        Stack<Integer> stackA;
        Stack<Integer> stackB;
    
        public MinStack() {
            stackA = new Stack<>();
            stackB = new Stack<>();
        }
    
        public void push(int val) {
            stackA.push(val);
            if (!stackB.isEmpty()) {//有就和栈顶比较
                //判断是否比栈顶要小
                if (stackB.peek() >= val) {
                    stackB.push(val);
                }
            } else {//没有就直接入栈
                stackB.push(val);
            }
        }
    
        public void pop() {
            int temp = stackA.pop();
            if (stackB.peek() == temp) {
                stackB.pop();
            }
        }
    
        public int top() {
            return stackA.peek();
        }
    
        public int getMin() {
            return stackB.peek();
        }
    }
    
求出最大公约数

计算两个数之间的最大公约数

  1. 辗转相除法
    a % b = c − − − > b % c = d − − > c % d = e a\%b=c--->b\%c=d-->c\%d=e a%b=c>b%c=d>c%d=e

  2. 更相减损法

a − b = c − − > b − c = d − − > c − d = e a-b=c-->b-c=d-->c-d=e ab=c>bc=d>cd=e

  • 综合使用,当两者是偶数时,可以对两者进行移位(左/右 1位)(乘/除 2),因为此时肯定是可以得到是2的某个次数
  • 通过&位的操作判断奇偶数
2的整数次幂

通过位运算的使用,2的整数次幂最高位都是为1,因为乘以2就相当于进行左移一位

那么通过位运算就可以得到是否为2的整数次幂
r e t u r n ( n u m & n u m − 1 ) = = 0 ; return (num\&num-1) == 0; return(num&num1)==0;

  • 位运算可以实现更快的速度并且契合数据在计算机中的储存形式。
无序数组排序后的最大相邻差

  • 计数排序

    面对无序的数组,通过构建新的索引统计数组,可以实现数组的有序化。

    但是不能处理范围摆动过大的数组,不然会浪费很多空间

  • 桶排序

    1. 通过划分桶的划分,在装入data的时候更新该桶的最值,那么在桶范围内的元素并就自然失去了决定性作用(实际上也不需要,因为起决定作用的是桶内的最值)

    2. 从第二个桶开始,比较当前桶的最大值和前一个的最小值的差距,更新返回值

public static int getMaxSortedDistance(int[] array) {
    int max = array[0], min = array[0];
    for (int i : array) {
        if (i > max) max = i;
        else if (i < min) min = i;
    }
    int d = max - min;
    if (d == 0) return 0;
    int bucketNum = array.length;
    Bucket[] buckets = new Bucket[bucketNum];
    for (int i = 0; i < bucketNum; i++) {
        buckets[i] = new Bucket();
    }
    for (int j : array) {
        //计算当前数在哪个桶内
        int index = ((j - min) * (bucketNum - 1) / d);
        //更新该桶的最值情况
        if (buckets[index].min == null || buckets[index].min > j) {
            buckets[index].min = j;
        }
        if (buckets[index].max == null || buckets[index].max < j) {
            buckets[index].max = j;
        }
    }
    int res = 0;
    //从第二个桶开始,进行计算更新返回值
    for (int i = 1; i < buckets.length; i++) {
        if (buckets[i].min == null) continue;//如果该桶没有最小值,那么没有比较,直接跳过说明没有值
        res = Math.max(res, buckets[i].max - buckets[i - 1].min);
    }
    return res;
}

private static class Bucket {

    public Integer min;

    public Integer max;
}
用栈实现队列
  • 使用备用栈进行来回的存储来取出原来在栈低的数值,取完再放回去,如果是存就直接加入栈
class MyQueue {
    private Stack<Integer> stack1;
    private Stack<Integer> stack2;
    private int front;

    /** Initialize your data structure here. */
    public MyQueue() {
        stack1 = new Stack<>();
        stack2 = new Stack<>();
    }

    /** Push element x to the back of queue. */
    public void push(int x) {
        if (stack1.isEmpty())
            front = x;
        stack1.push(x);

    }

    public int  pop(){
        if(stack2.isEmpty()){
            while(!stack1.isEmpty()){
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop();
    }

    /** Get the front element. */
    public int peek() {
        if(!stack2.isEmpty()){
            return stack2.peek();
        }
        return front;

    }

    /** Returns whether the queue is empty. */
    public boolean empty() {
        return stack1.isEmpty()&&stack2.isEmpty();

    }
} 
寻找全排列的下一个数

在一个整数所包含数字的全部组合中,找到一个大于且仅大于原数的新整数

  • 要点
    1. 尽量保持最高位的不变,因此需要对低位进行遍历获取低位上的逆序部分
    2. 将将逆序当中比较小的但是比高位大的那个与前一位高位进行交换
    3. 逆序部分进行排序

这样就能最小地变换最小的那个高位,保证逆序区域的最大

class Solution {
    public void nextPermutation(int[] nums) {
        //返回要交换的高位
        int index = findTransferPoint(nums);
        //如果为0,说明已经是最小的那个,进行翻转
        if (index == 0) {
            nums = reverse(nums, 0);
            return;
        }
        //交换高位
        nums = exchangeHead(nums, index);
        //将逆序部分进行排序,就是翻转
        nums = reverse(nums, index);
    }

    private int[] reverse(int[] exnums, int index) {
        for (int i = index, j = exnums.length - 1; i < j; i++, j--) {
            int temp = exnums[i];
            exnums[i] = exnums[j];
            exnums[j] = temp;
        }
        return exnums;
    }

    private int[] exchangeHead(int[] nums, int index) {
        int head = nums[index - 1];
        for (int i = nums.length - 1; i > 0; i--) {
            if (nums[i] > head) {
                nums[index - 1] = nums[i];
                nums[i] = head;
                break;
            }
        }
        return nums;
    }

    private int[] reverseCopy(int[] nums) {
        int[] newCopy = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            newCopy[i] = nums[nums.length - 1 - i];
        }
        return newCopy;
    }

    private int findTransferPoint(int[] nums) {
        for (int i = nums.length - 1; i > 0; i --) {
            if (nums[i] > nums[i - 1]) {
                return i;
            }
        }
        return 0;
    }
}
删去k个数字后的最小值
  • 针对正序的第一个小于前一个的数进行删除
  • 要点
    1. 通过栈进行可用字符的装入
    2. 判断是否要将前一个数进行删除
    3. 查找偏移值(第一个非0的位置)
class Solution {
    public 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--;
                //可用次数减小一个
                k--;
            }
            //不符合删除条件就加入
            stack[top++] = c;
        }
        //找到第一个非零数字的数值
        int offset = 0;
        while (offset < newLength && stack[offset] == '0') {
            offset++;
        }
        //从偏移位置开始生成新的字符串
        return offset==newLength?"0":new String(stack,offset,newLength-offset);
    }
}
如何实现大整数相加
  • 针对超大数字的计算,此时数据类型已经没有办法进行保存和记录
  • 使用字符串进行对数字的保存,用过构建数组进行数值的加减计算,结果数组需要比较长数组的位数多一位,保持进位的空位。
缺失的整数

通过异或的操作

算法的实际使用

Bitmap的使用
  • 要点
    1. 类似于热编码一样,通过构建字典来完成数据标签的索引
    2. 通过唯一标识的主键id,将数据用位的1和0进行表征,代表这个标签的有无。
    3. 通过位运算进行标签的聚合以及对象属性的计算
LRU算法的应用

在日常的运作当中,很多信息都会进行读取,但是当访问量大的时候就会形成一定的问题

因此通常会考虑将访问较为频繁的信息数据保存在缓存当中,不需要进入数据库进行读取

那么就需要考虑如果进行数据的存放

Least Recently Used

通过链式哈希表进行实现

  • 取用
    • 如果原来有—将数据调到最前
    • 如果没有—在头部进行创建
      • 如果已经超出了范围,将尾部删除
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Little BOY.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值