代码随想录算法训练营 DAY6| 242.有效的字母异位词 349.两个数组的交集 202.快乐数 1.两数之和

哈希表理论基础

哈希表是根据关键码的值而直接进行访问的数据结构。

一般哈希表都是用来快速判断一个元素是否出现集合里

如果使用哈希表的话, 只需要O(1)就可以做到查询一个元素。

哈希函数

哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。

在这里插入图片描述

哈希碰撞

如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞

在这里插入图片描述

一般哈希碰撞有两种解决方法, 拉链法和线性探测法。

拉链法

相当于数组+链表的结合。

需要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。

在这里插入图片描述

线性探测法

使用线性探测法,一定要保证tableSize大于dataSize

冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。

在这里插入图片描述

java中常见的哈希结构

  • 数组
  • set(集合)
  • map(映射)

HashMap

在 HashMap 中的加载因子为 0.75。

  1. HashMap是基于哈希表的Map接口的非同步实现,
  2. 允许使用null值和null键(HashMap最多只允许一条记录的键为null,允许多条记录的值为null。)。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
  3. HashMap中不允许出现重复的键(Key)
  4. Hashmap是非线程安全的,
  5. 其迭代器是fail-fast的
  6. HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体,(JDK1.8增加了红黑树部分,会将时间复杂度从O(n)降为O(logn))。
  7. 数据存储:先根据key的hashCode(使用key的hashCode()方法获取)重新计算hash值,根据hash值算出这个元素在数组中的位置(即下标), 如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
  8. 数据读取:首先根据key的hashCode,找到其数组中对应位置的数据(可能只有一个数据,也可能是多个数据,其表现形式是一个链表),然后通过key的equals方法在对应位置的链表中找到需要的元素。
  9. hashMap的默认初始容量是16个,其会有一个负载因子,用于当hashMap中的数据量等于容量*负载因子时,hashMap会进行扩容,扩大的容量是原本的2倍。负载因子的默认初始值为0.75

这里写图片描述

HashSet

  1. 它是基于HashMap实现的,底层采用HashMap来保存元素,而且只使用了HashMap的key来实现各种特性。HashSet实现了Set接口
  2. HashSet较HashMap来说比较慢
  3. HashSet中的数据不是key-value键值对,其只是单值,虽然其借助与HashMap来实现,但是其只是将值作为key来存入HashMap中,因为HashMap中的值是key-value键值对的,所以每个HashSet存储到HashMap的数据对应的value值只是一个new Object()对象
  4. 当添加数据时,如果set中尚未包含指定元素,则添加指定元素。更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2))的元素e2,则向此set 添加指定的元素e。如果此set已包含该元素,则该调用不更改set并返回false。但底层实际将将该元素作为key放入HashMap。

Hashtable

  1. Hashtable也是一个散列表,它存储的内容是键值对。基于Dictionary类
  2. 存储数据: 首先判断value是否为空,为空则抛出异常;计算key的hash值,并根据hash值获得key在table数组中的位置index,如果table[index]元素不为空,则进行迭代,如果遇到相同的key,则直接替换,并返回旧value;否则,我们可以将其插入到table[index]位置。
  3. key和value都不允许为null,Hashtable遇到null,直接返回NullPointerException。
  4. 线程安全,几乎所有的public的方法都是synchronized的
  5. 较HashMap速度慢

LinkedHashMap

  1. LinkedHashMap是HashMap的一个子类,它保留插入顺序,帮助我们实现了有序的HashMap。
  2. 其维护一个双向链表,并不是说其除了维护存入的数据,另外维护了一个双向链表对象,而是说其根据重写HashMap的实体类Entry,来实现能够将HashMap的数据组成一个双向列表,其存储的结构还是数组+链表的形式,
  3. LinkedHashMap能够做到按照插入顺序或者访问顺序进行迭代顺序。
  4. 修改Entry对象,Entry新增了其上一个元素before和下一个元素after的引用
  5. 根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表
  6. LinkedHashMap并未重写父类HashMap的put方法,只是重写了put方法里面的recordAccess、addEntry、createEntry等方法,添加了特有的双向链接列表
  7. LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。
  8. 这个虽然是访问元素,但是当设置以访问排序时,其仍然会先将元素从原本位置remove掉,然后在将该元素以新元素插入到链头,哪怕其新插入的位置还在原位置(所以如果以访问排序,其过程会涉及到删除数据和增添数据)。
  9. 读取速度与容量无关

TreeMap

在这里插入图片描述

  1. TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。 该映射根据其键的自然顺序(字母排序)进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  2. TreeMap是非线程安全的。 它的iterator 方法返回的迭代器是fail-fast的。

ConcurrentHashMap

  1. ConcurrentHashMap是弱一致性,也就是说遍历过程中其他线程可能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据
  2. ConcurrentHashMap是基于分段锁设计来实现线程安全性,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。
  3. 并发度就是ConcurrentHashMap中的分段锁个数,默认的并发度为16,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)
  4. 通过将key的高n位(n = 32 – segmentShift)和并发度减1(segmentMask)做位与运算定位到所在的Segment(分段)

HashMap相关方法

1.1 初始化

HashMap<object1, object2> hashmap = new HashMap<object1, object2>();

1.2 添加新键值对

hashmap.put(object1, object2);

1.3 在原有的基础上增加

hashmap.put(object1, hashmap.getOrDefault((object1, 0) + 1));

1.4 访问元素的value

object2 obj2 = hashmap.get(object1);

1.5 判断存在

boolean contains(Object value) ——-确定哈希表内是否包含了给定的对象,若有返回true,否则false
Boolean bool = hashmap.containsKey( Object key );
Boolean bool = hashmap.containsValue( Object value );

1.6 删除元素

hashmap.remove(object1);

1.7 清空元素

hashmap.clear();

1.8 计算大小

int size = hashmap.size();

1.9 遍历

for(object1 obj1 : hashmap.keySet()){
System.out.println(obj1 + hashmap.get(obj1));
}
for(object2 obj2 : hashmap.values()){
System.out.println(obj2);
}

1.10 判断为空

Boolean bool = hashmap.isEmpty();

HashSet相关方法

HashSet继承自Set接口

  • 初始化
Set<Integer> set1 = new HashSet<>();
  • 添加元素
set1.add(i);
  • 将set集合转为数组
resSet.stream().mapToInt(x -> x).toArray();
  • 判断set中是否存在该元素
set1.contains(i)
  • set集合元素的个数
set1.size()

242.有效的字母异位词

题目链接:https://leetcode.cn/problems/valid-anagram/

视频链接:https://www.bilibili.com/video/BV1YG411p7BA/?spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689

讲解链接:https://programmercarl.com/0242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE

暴力

两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。

思路

在这里插入图片描述

因为都是小写字母,考虑用一个长度为26的数组,a~z对应hash[0,25]

第一遍for循环先统计s中每个字符出现的次数,第二次for遍历t,对应字符出现一次 就让hash[]--。最后看hash数组是否全为0,是就说明是有效的字母异位词。

hash[s[i] - 'a']++;      //遍历s的操作

hash[t[i] - 'a']--;      //遍历t的操作
  • 完整java代码:
class Solution {
    public boolean isAnagram(String s, String t) {
        int[] record = new int[26];
        for(int i = 0; i < s.length(); i++) {
            record[s.charAt(i) - 'a']++;
        }
        for(int i = 0; i < t.length(); i++) {
            record[t.charAt(i) - 'a']--;
        }
        for(int count : record) {
            if(count != 0) {
                return false;
            }
        }
        return true;
    }
}

349.两个数组的交集

题目链接:https://leetcode.cn/problems/intersection-of-two-arrays/description/

视频链接:https://www.bilibili.com/video/BV1ba411S7wu/?spm_id_from=333.788&vd_source=80cf8293f27c076730af6c32ceeb2689

讲解链接:https://programmercarl.com/0349.%E4%B8%A4%E4%B8%AA%E6%95%B0%E7%BB%84%E7%9A%84%E4%BA%A4%E9%9B%86.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE

思路

题目说length<=1000,其实用数组也可以。但是要求去重,所以用HashSet

先对nums1进行处理,存放到哈希表里,然后遍历nums2,如果哈希表中也出现过,就放到result集合里,注意是去重的。

需要注意 如果有一个数组为null,就返回一个空数组!

  • 数组解法:
hash[nums1.charAt(i)] = 1;
  • java代码(用set)
class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        if(nums1 == null || nums2 == null) {
            return new int[0];
        }
        Set<Integer> retSet = new HashSet<>();
        Set<Integer> set = new HashSet<>();
        for(int i : nums1) {
            set.add(i);
        }
        for(int i : nums2) {
            if(set.contains(i)) {
                retSet.add(i);
            }
        }
        //新申请一个数组用来返回
        int[] result = new int[retSet.size()];
        int j = 0;
        for(int i : retSet) {
            result[j++] = i;
        }
        return result;
    }
}
  • 版本二:用数组
class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        int[] hash1 = new int[1000];
        int[] hash2 = new int[1000];
        for(int i : nums1) {
            hash1[i]++;
        }
        for(int i : nums2) {
            hash2[i]++;
        }
        //动态添加元素 用ArrayList
        List<Integer> result = new ArrayList<>();
        for(int i = 0; i < 1000; i++) {
            if(hash1[i] > 0 && hash2[i] > 0) {
                result.add(i);  //注意i代表的是重复了的元素 hash1[i]是出现的次数
            }
        }
        //最后还要转成数组返回
        int[] res = new int[result.size()];
        int j = 0;
        for(int i : result) {
            res[j++] = i;
        }
        return res;
    }
}

202.快乐数

题目链接:https://leetcode.cn/problems/happy-number/

讲解链接:https://programmercarl.com/0202.%E5%BF%AB%E4%B9%90%E6%95%B0.html

思路

题目中说平方和可能会无限循环,所以只要一旦开始循环了就一定不是快乐数。

可以把每一次得到的平方和sum放入set集合里,每次放入前判断一次set.contains(sum)!=true,如果set中已经有了就return false,一旦出现1就return true

  • 注意先判断是否重复再判断1!

  • 怎么取每个位上的数?

while(n > 0) {
            sum += (n % 10) * (n % 10);
            n = n / 10;
        }

把计算sum单独写成一个方法,简化代码。

  • java代码
class Solution {
    public int getSum(int n) {
        int sum = 0;
        while(n > 0) {
            sum += (n % 10) * (n % 10);
            n = n / 10;
        }
        return sum;
    }

    public boolean isHappy(int n) {
        Set<Integer> set = new HashSet<>();
        while(true) {
            int sum = getSum(n);
            if(set.contains(sum)) {
                return false;
            }
            if(sum == 1) {
                return true;
            }
            set.add(sum);
            n = sum;  //记得更新sum
        }
    }
}

1.两数之和

题目链接:https://leetcode.cn/problems/two-sum/description/

视频链接:https://www.bilibili.com/video/BV1aT41177mK/?spm_id_from=333.788

讲解链接:https://programmercarl.com/0001.%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C.html#%E6%80%9D%E8%B7%AF

暴力法

两层for,遍历每一个数和其他的数相加。

思路

四个重点:

  • 为什么会想到用哈希表
  • 哈希表为什么用map
  • 本题map是用来存什么的
  • map中的key和value用来存什么的

首先什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。在这题里就是我们遍历数组时要找target-nums[i]是否在集合里出现过。

本题需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。

  • 选用什么形式的哈希表?

存放这个元素的同时,还要存放对应的下标,因此选用Map。key对应数组元素,value对应下标。

在这里插入图片描述

在这里插入图片描述

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] result = new int[2];
        Map<Integer,Integer> map = new HashMap<>();
        for(int i = 0; i < nums.length; i++) {
            if(map.containsKey(target - nums[i])) {
                result[0] = i;
                result[1] = map.get(target - nums[i]);  //获取对应的下标
                break;
            }
            map.put(nums[i],i);
        }
        return result;
    }
}

day6总结

1.java中 对字符串操作:

str.length()
str.charAt()

2.选择数组,set,map的原则

  • 元素比较少,范围可控,元素较为连续 —选数组
  • 数值比较大,元素很分散 —选set
  • 有key value对应 —选map

3.数组如果要动态添加元素,就要用ArrayList。

4.如何取每个位置上的单数?

  • while(n > 0) {

    ​ sum += (n % 10) * (n % 10);

    ​ n = n / 10;

    ​ }

5.如果有一个重复性高的需求,考虑另外单独写成一个方法。

  • 如:getSum()方法 求每个位置上数的平方和

6.HashSet对应的方法

//初始化
Set<Integer> set = new HashSet<>();
//添加元素
set.add(...);
//返回set的大小
set.size();
//查询是否含有某元素
set.contains();

7.HashMap对应的方法

//初始化
Map<Integer,Integer> map = new HashMap<>();
//添加键值对
map.put(key,value);
//判断key是否存在
map.containskey(key);
//根据key获取对应value
map.getValue(key);
  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值