数据结构-死磕王争

数组下标为0是为了减少根据下标寻址时-1的运算。

链表技巧:

  1. 认清指针的概念,Java Python的引用就是指C语言里的指针。将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
  2. 警惕指针丢失和内存泄露:插入节点要注意操作顺序,删除链表是要手动释放内存空间。
new_node->next = p->next;
p->next = new_node;
  1. 利用哨兵简化实现难度
  2. 处理边界:链表为空、只包含一个node、只包含2个node、处理头结点和尾节点时能否正常工作。
  3. 画图举例
  4. 训练 单链表反转链 表中环的检测 两个有序的链表合并 删除链表倒数第 n 个结点 求链表的中间结点

操作受限的线性表,只允许一端插入和删除,先进后出,后进先出,比如浏览器后退功能
数组的动态扩容 – 重新申请连续的2倍空间,把原来的数据复制进去
浏览器后退前进的功能,使用两个栈分别进行压栈和出栈操作。

队列

入队 enqueue 加入队尾 出队 dequeue 取出头部 阻塞队列 并发队列 disruptor

递归 动态规划-自底向上

规模更小的子问题
递推公式 终止条件
f(n) = f(n-1) + f(n-2)

排序

稳定排序 保证相同数据对应的订单顺序不变,先按照时间排序再用稳定排序对金额排序
原地排序空间复杂度O(1) 冒泡排序遇到两个相同元素时不做交换,是稳定性算法
插入排序也是原地排序,是稳定性算法
选择排序不是稳定性算法,是原地排序在这里插入图片描述
归并排序 快速排序 O(nlogn)
分治是一种解决问题的处理思想,递归是一种编程技巧,二分思想
合并方法,两个指针分别指向分区的头,比较存入temp数组,最后再倒回原数组
快速排序 分而治之 选择分区点pivot的方法思路:
归并排序的处理过程是由下到上的,先处理子问题,然后再合并。而快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题
归并排序稳定但不是原地排序,快排不稳定但是原地排序
在这里插入图片描述
桶排序,计数排序都是线性排序,计数排序步骤:
待排数组a[n] 遍历a算出最大数m,申请c[m],遍历a将a的值作为c的下标计数存入,依次累加c斐波那契方式,申请r[n],倒序遍历a – 计算r下标的方法为c[a[i]] - 1 存入相应位置,c[a[i]]-- 相应位置的值减一,因为相同的数组已经挪过去一个。
基数排序

public class Solution{
	public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        int maxDigit = getMaxDigit(arr);
        return radixSort(arr, maxDigit);
    }
    /**
     * 获取最高位数
     */
    private int getMaxDigit(int[] arr) {
        int maxValue = getMaxValue(arr);
        return getNumLenght(maxValue);
    }
    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }
    protected int getNumLenght(long num) {
        if (num == 0) {
            return 1;
        }
        int lenght = 0;
        for (long temp = num; temp != 0; temp /= 10) {
            lenght++;
        }
        return lenght;
    }
    private int[] radixSort(int[] arr, int maxDigit) {
        int mod = 10;
        int dev = 1;
        for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
            // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
            int[][] counter = new int[mod * 2][0];
            for (int j = 0; j < arr.length; j++) {
                int bucket = ((arr[j] % mod) / dev) + mod;
                counter[bucket] = arrayAppend(counter[bucket], arr[j]);
            }
            int pos = 0;
            for (int[] bucket : counter) {
                for (int value : bucket) {
                    arr[pos++] = value;
                }
            }
        }
        return arr;
    }
    /**
     * 自动扩容,并保存数据
     *
     * @param arr
     * @param value
     */
    private int[] arrayAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }
}

排序优化:java使用堆排序, C使用快速排序。快速排序最坏复杂度为O(n2),设置递归深度防止溢出,qsort方法会判断大小,小于100m的用归并空间换时间,大于用快排。

跳表

redis sorted set 使用了跳表,放弃红黑树在这里插入图片描述
链表加多级索引的机构就是跳表,跳表区间查询比红黑树高。
索引空间可以忽略,插入删除操作时间复杂度O(logn)
红黑树,avl树,跳表都是动态数据结构,通过左旋右旋维护平衡
增删查效率一样,范围查询跳表比红黑树效率高可以做到O(logn)

二分查找
public int bsearch(int[] a, int n, int value) {
  int low = 0;
  int high = n - 1;

  while (low <= high) {
    int mid = (low + high) / 2;
    if (a[mid] == value) {
      return mid;
    } else if (a[mid] < value) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }

  return -1;
}

循环退出条件 是low<=high 不是小于
mid的取值mid=(low+high)/2 优化后 low+(high-low)/2 位运算low+((high-low)>>1)
low high的更新
二分查找限制 数组 有序 大型数据量时候
二分查找变种 重复元素 查找第一个等值元素,mid元素等于value 的情况再加个条件上个节点值不等于, 同理查找最后一个元素时加下个节点不等于

散列表:

把对象转化为数组下标的方法称为散列函数
散列冲突:
开放寻址法 - 有冲突线性寻找空闲位置,排除标记为deleted的位置放置hash失效
二次探测 探测下标成倍 双重探测 使用多个hash方法
装载因子 - 散列表中的空闲个数
链表法 - 每个槽位对应一个链表,冲突的话从链表尾部插入
散列冲突会导致效率大幅下降,极端会退化为链表,怎么破
算法不要太复杂,对于频繁变动的集合,可以动态扩容来降低装载因子
装载因子阈值权衡,追求效率就可以设的低一点,最求内存使用率就可以设置高点
避免低效扩容:扩容后一次性转移数据会低效,分批转移可解决此问题
ThreadLocalMap使用寻址法的原因 数据量小,装载因子小小于1,序列化简单
拉链法对装载因子大于1的容忍度高,hashmap使用拉链法适合大对象,大数据量的散列表,可用红黑树和跳表代替链表。
hashmap 默认大小 16 装载因子0.75 长度大于8转化为红黑树

int hash(Object key) {
    int h = key.hashCode()return (h ^ (h >>> 16)) & (capicity -1); //capicity表示散列表的大小
}
public int hashCode() {
  int var1 = this.hash;
  if(var1 == 0 && this.value.length > 0) {
    char[] var2 = this.value;
    for(int var3 = 0; var3 < this.value.length; ++var3) {
      var1 = 31 * var1 + var2[var3];
    }
    this.hash = var1;
  }
  return var1;
}

散列表和链表结合使用, LRU缓存淘汰算法
在这里插入图片描述
prev next节点是串联双向链表的, hnext指针是串联散列表节点的
查找很快 O(1) 删除也一样, 插入特殊
查询缓存,如果存在,现将其移动到链表尾部,不存在,判断缓存没有满直接放到尾部,满了,删除头部在放尾部
LinkedHashMap本身就支持LRU,重要性排行榜:连续空间 > 时间 > 碎片空间。

复杂度分析

事后统计法 受环境和数据规模影响很大
大O复杂度 所有代码的执行时间 T(n) 与每行代码的执行次数成正比。
代码执行时间随数据规模增长的变化趋势。T(n)=O(f(n))
只关注循环执行次数最多的那一段代码就可以了,所以一般一个循环就是O(n)
如果 T1(n)=O(f(n)),T2(n)=O(g(n));
那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))).
假设 T1(n) = O(n),T2(n) = O(n2),则 T1(n) * T2(n) = O(n3)
在这里插入图片描述
多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2n) 和 O(n!)。
在采用大 O 标记复杂度的时候,可以忽略系数,即 O(Cf(n)) = O(f(n))
渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
复杂度分析关键在于多练,孰能生巧耳。
最好情况时间复杂度(best case time complexity)、
最坏情况时间复杂度(worst case time complexity)、
平均情况时间复杂度(average case time complexity)、
均摊时间复杂度(amortized time complexity)
一般均摊时间复杂度就等于最好情况时间复杂度。
出现O(1)的次数远大于出现O(n)出现的次数,那么平均平摊时间复杂度就是O(1)。。。。

哈希算法

任意长度的二进制值串映射为固定长度的二进制值串,这个映射的规则就是哈希算法
通过原始数据映射之后得到的二进制值串就是哈希值
设计要点:单向,敏感,散列冲突概率小,高效
MD5(MD5 Message-Digest Algorithm,MD5 消息摘要算法)
SHA(Secure Hash Algorithm,安全散列算法)
应用:加密,唯一值,校验,散列函数 算法一般简单,不关心是否单向
区块链:区块头保存自己的区块体和上个区块头的哈希值,使用SHA256哈希算法。计算哈希值非常耗时,如果要篡改一个区块,就必须重新计算该区块后面所有的区块的哈希值,短时间内几乎不可能做到。
通过哈希算法,对客户端 IP 地址或者会话 ID 计算哈希值,将取得的哈希值与服务器列表的大小进行取模运算,最终得到的值就是应该被路由到的服务器编号。 这样,我们就可以把同一个 IP 过来的所有请求,都路由到同一个后端服务器上。???
一致性哈希算法:我们就将某几个小区间的数据,从原来的机器中搬移到新的机器中。这样,既不用全部重新哈希、搬移数据,也保持了各个机器上数据数量的均衡。
二叉树删除实现:标记已删除节点,取巧方式。
散列表无序,扩容耗时,散列设计困难,平衡因子也不能太高。

红黑树

根节点是黑色
叶子节点为NIL,不存储数据
两个红色节点不挨着
每个节点到达所有叶子节点路上的黑节点个数相同
AVL树是高度平衡二叉树,查找快,插入删除旋转费劲

旋转数组思路,嵌套循环挨个交换,新建数组取模存入,反转,先总体反转,再局部反转0,k-1 k,len-1
gcd(ll a,ll b){ return b==0?a:gcd(b,a%b); } 求两个数的最大公约数

public class Solution{
	public void rotate(int[] nums, int k){
	   int len = nums.length
	   k = k%len;
	   reverse(nums, 0, len - 1);
	   reverse(nums, 0, k - 1);
	   reverse(nums, k, len - 1);
	 }
     public void reverse(int[] nums, int start, int end){
		while(start < end){
			int temp = nums[start];
			nums[start] = nums[end];
			nums[end] = temp;
			start++;end--;
		}
	 }
}

在这里插入图片描述

递归树分析时间复杂度
堆排序

堆的特性:是一颗完全二叉树,父节点大于等于或者小于等于子节点,大堆顶小堆顶,堆化过程自下向上挨个比较交换,交换i 和 i/2元素。删除堆顶逻辑,把最后一个元素当做栈顶,自上而下堆化。
建堆,对下标从 2n​ 开始到 1 的数据进行堆化,叶子节点不需要堆化。
排序,依次取堆顶,重新堆化,再取堆顶只到剩一个值。
堆访问是跳着访问对cpu不友好

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值