专栏简介 :java语法及数据结构
题目来源:leetcode,牛客,剑指offer
创作目标:从java语法角度实现底层相关数据结构,达到手撕各类题目的水平.
希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.
学历代表过去,能力代表现在,学习能力代表未来!
目录
前言
Map与Set作为数据结构中的搜索算法其重要性不言而喻,让我一起来深入探索一下吧!
一>Map与Set概述
1.简介:
Map与Set是一种专门用来搜索的数据结构或容器,其搜索的效率与其具体的实现类有关.
在此之前常见的搜索方式有,
- 直接搜索时间复杂度为O(N),如果元素过多会非常慢.
- 二分查找时间复杂度为O(logN)但要求查找元素必须有序,
并且以上方法只适用于静态类型的查找而实际生活中常常涉及到增删的动态操作
如:
- 找到通讯录中的联系人并修改.
- 找到学校数据库中学生信息,更新其每一学年的成绩信息.
这时就需要Map与Set这类动态的查找集合容器.
2.常见模型:
一般将搜索的数据关键字称为Key,关键字对应值Value,将其称之为Key-Value键值对.
1.Set型(纯key)
- 快速查找词典中是否含有某词语.
- 通讯录中查找联系人.
2.Map型(Key-Value)
- 统计词典中词语出现的频率.
3.Map
1.Map的说明:
- 由上图可以看出,Map没有继承自Collection类,所以它不具备遍历自身的方法,使用for each方法遍历集合类的前提是该集合类必须实现Iterable接口.
- Map的具体实现有TreeMap和HashMap.TreeMap底层为红黑树,HashMap底层为哈希表.TreeMap实现了接口SortedMap,所以TreeMap中的Key一定是可比较的.
2.Map接口中常用方法:
public class Review_Map { public static void main(String[] args) { Map<String,Integer> map = new TreeMap<>(); //1. V put(K key, V value) 设置 key 对应的 value map.put("Hello",3); map.put("world",2); map.put("hahahai",2); //2. V get(Object key) 返回 key 对应的 value System.out.println(map.get("Hello"));//"3" System.out.println(map.get("world"));//"2" //3. V getOrDefault(Object key, V defaultValue) 返回 key 对应的 value,key 不存在,返回默认值 map.getOrDefault("babiQ",-1);//-1 //4. V remove(Object key) 删除 key 对应的映射关系,并返回value. System.out.println(map.remove("Hello"));//"3" //5. Set<K> keySet() 返回所有 key 的不重复集合 ,因为key不可重复所以使用Set接收 Set<String> set = map.keySet();//"Hello" "world" "hahahai" //6. Collection<V> values() 返回所有 value 的可重复集合 Collection<Integer> collection = map.values();//3 2 2 //7. Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射关系 Set<Map.Entry<String,Integer>> entrySet = map.entrySet(); // "Hello",3 "world",2 "hahahai",2 //8. boolean containsKey(Object key) 判断是否包含 key System.out.println(map.containsKey("Hello"));//true //9. boolean containsValue(Object value) 判断是否包含Value System.out.println(map.containsValue(3));//true } }
- Tips:TreeMap中存储元素时一定是可以比较的,如果没法比较就会抛类型捕获异常(ClassCastException)
3.Map.Entry<K,V>的说明:
- Map.Entry<K,V>是Map内部实现的,用来存放<Key,Value>键值对映射关系的内部类.
- 实际上就是将Map的映射关系组装起来,存在Set集合中.这样就可以遍历组装好的Map.
- 该类中主要提供了Key,Value的获取和Value的修改,没有Key的修改.
Set<Map.Entry<String,Integer>> entrySet = map.entrySet(); for (Map.Entry<String,Integer> entry:entrySet) { System.out.println("Key :"+entry.getKey()+"Value :"+entry.getValue()); }
4.注意事项:
- Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap,HashMap.
- Map中存放的键值对Key是唯一的(因为底层是一颗搜索树),而Value可以重复.
- Map中的Key可以全部分离出来,存放在Set中进行访问.(Set中元素不能重复).
- Map中的Value可以全部分离出来,存放在Collection的任何一个子集合中.
- Map中的Key不能直接修改,若想修改只能删除后重新插入.
- TreeMap中插入键值对时key不能为空,否则会抛空指针异常,value可以为空.HashMap中key和value都可以为空.
4.Set
1.Set的说明:
- 由上图可知,Set继承自Collection接口类,意味着它可以遍历自身.它的实现类是TreeSet和HashSet.TreeSet实现了SortedSet接口,说明其内部元素可比较.
- Set与Map的不同点就是Set中只存储key且继承自Collection接口类.
2.Set的使用:
public static void main(String[] args) { Set<String> set = new TreeSet<>(); //1.boolean add(E e) 添加元素,但重复元素不会被添加成功 set.add("Hello"); set.add("world"); set.add("hahahai"); //2.boolean contains(Object o) 判断 o 是否在集合中 System.out.println(set.contains("world"));//true //3.Iterator<E> iterator() 返回迭代器 Iterator<String> iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()+" "); } //4. boolean remove(Object o) 删除集合中的 o set.remove("hahahai"); //5. int size() 返回set中元素的个数 System.out.println(set.size());//2 //6. boolean isEmpty() 检测set是否为空,空返回true,否则返回false System.out.println(set.isEmpty());//false //7.Object[] toArray() 将set中的元素转换为数组返回 Object[] array = set.toArray(); //8.boolean containsAll(Collection<?> c) 集合c中的元素是否在set中全部存在,是返回true,否则返回false Collection<String> c = new ArrayList<>(); c.add("Hello"); c.add("world"); System.out.println(set.containsAll(c)); //9. boolean addAll(Collection<? extendsE> c) 将集合c中的元素添加到set中,可以达到去重的效果 Collection<String> d = new ArrayList<>(); d.add("hahaai"); set.addAll(d);//"Hello" "world" "hahahai" //10.void clear() 清空集合 set.clear(); }
3.关于Set中元素不能重复的说明:
4.注意事项:
- Set是继承自Collection的一个接口;
- Set中只存储key,且要求key是唯一不重复的;
- TreeSet的底层是TreeMap实现的,添加元素时其底层使用key与Object的一个默认对象作为键值对插入到Map中;
- Set最大的功能就是去重;boolean addAll(Collection<? extendsE> c)
- 实现Set的常见接口除了TreeSet和HashMap之外,还有LinkedHashSet,其在HasSet的基础上维护一个双向链表来记录元素的插入次序.
- Set中元素不能修改,如果要修改只能删除后重新插入.
二>.搜索树
Tips: Map与set的底层实现逻辑是一颗红黑树, 红黑树本质是搜索树但其实现逻辑较为复杂,本文采用二插搜索树来理解Map与Set的背后实现逻辑.
1.二插搜索树的概念:
- 若其左子树不为空,则左子树上所有节点的值均小于根节点的值.
- 若其右子树不为空,则右子树上所有节点的值均大于根节点的值.
- 同理,其左右子树也为二插搜索树.
2.二插搜索树的基本操作:
1)查找
- 定义一个变量cur遍历搜索树,如果比当前节点大的就去右子树找,如果比当前节小就去左子树找.
public class BinarySearchTree { static class TreeNode{ public int val; public TreeNode left; public TreeNode right; public TreeNode(int val) { this.val = val; } } public TreeNode root = null; /** * 查找 * 最好O(logn) 最坏O(N) */ public TreeNode search(int val){ if(root.val==val)return root; TreeNode cur = root; while (cur!=null){ if(cur.val<val){ cur = cur.right; }else if(cur.val>val){ cur = cur.left; }else { return cur; } } return null; }
2)插入
- 首先需要明确搜索树的插入操作, 只能插入到叶子节点下面.
- 其次定义变量cur遍历搜索树, 如果比当前节点大的就去右子树找, 如果比当前节小就去左子树找. 直到cur==null,说明找到了插入位置, 该位置就是cur的上一个节点但此时已无法返回所以需要再定义一个变量prev, 时刻记录cur的上一个位置.
- 当cur==null时, prev就是要插入的位置, 根据prev值的大小判断插入其左子树还是右子树即可.
/** * 插入 */ public TreeNode insert(int val){ TreeNode node = new TreeNode(val); if (root==null){ root = node; return root; } TreeNode cur = root; TreeNode prev = null; while (cur!=null){ if(cur.val<val){ prev = cur; cur = cur.right; }else if(cur.val>val){ prev = cur; cur = cur.left; }else { throw new RuntimeException("已存在该元素"); } } if(prev.val<val){ prev.right = node; }else if(prev.val>val){ prev.left = node; }else { throw new RuntimeException("已存在该元素"); } return root; }
3)删除
- 删除操作相对复杂, 首先还是定义变量cur 查找到需要删除元素的位置, 根据链表删除的经验, 删除一个节点一定需要知道其前驱节点parent.
- 1.cur.left(左树)==null, 若cur==root(根节点), root(根节点) = cur.right(右树). 若cur!=root(根节点), 如果parent的左子树是cur , 则parent.left=cur.right.如果parent的右子树是cur , 则parent.right=cur.right.
- 2.cur.right(右树)==null,同理.
- 3.cur的左树和右树都不为空, 采用"替罪羊法", 删除需要找到左树最大的元素或者右树最小的元素来替换,这样才符合二叉搜索树的原理.
/** * 删除 */ public void remove(int key) { /** * 1.cur.left==null * 1.cur==root,root = cur.right; * 2.cur!=root,cur==parent.left,parent.left = cur.right; * 3.cur!=root,cur==parent.right,parent.right = cur.right; */ /** * 2.cur.right==null * 1.cur==root,root = cur.left; * 2.cur!=root,cur==parent.left,parent.left = cur.left; * 3.cur!=root,cur==parent.right,parent.right = cur.left; */ /** * 3.cur.left!=null&&cur.right!=null "替罪羊法" */ TreeNode cur = root; TreeNode parent = null; while (cur != null) { if (cur.val == key) { removeNode(parent, cur); return; } else if (cur.val < key) { parent = cur; cur = cur.right; } else if (cur.val > key) { parent = cur; cur = cur.left; } } } public void removeNode(TreeNode parent, TreeNode cur) { if (cur.left == null) { if (cur == root) { root = cur.right; } else if (cur == parent.left) { parent.left = cur.right; } else { parent.right = cur.right; } } else if (cur.right == null) { if (cur == root) { root = cur.left; } else if (cur == parent.left) { parent.left = cur.left; } else { parent.right = cur.left; } } else { TreeNode target = cur.right;//找右树中最小的 TreeNode targetParennt = cur; while (target.left != null) { targetParennt = target; target = target.left; } cur.val = target.val; if (target == targetParennt.left) {//存在左树 targetParennt.left = target.right; } else {//不存在左树 targetParennt.right = target.right; } } } }
3.相关题目
- 根据二叉树搜索树的性质,采用中序遍历来搜索,在搜索的过程中改变节点的指向.
- 由于递归回溯的性质,可以定义全局变量prev和head,prev在临时变量cur回退的过程中修改节点之间的指向,head在cur第一次搜索到左子树最小节点时赋值.
- 最终当cur遍历完整个二插搜索树后,prev正好到达最后一个节点的位置.由于是双向循环链表,最终连接prev和head即可.
class Solution { Node prev,head; public void dfs(Node cur){ if(cur==null) return; dfs(cur.left); 此时cur在1的位置 if(prev!=null){ prev.right = cur; cur.left = prev; prev = cur; }else{ head = cur; prev = cur; } //进阶思路 // if(prev!=null){ // prev.right = cur; // }else{ // head = cur; // } // cur.left = prev; // prev = cur; dfs(cur.right); } public Node treeToDoublyList(Node root) { //搜索方式中序遍历 if(root==null) return null; dfs(root); head.left = prev; prev.right = head; return head; } }
4.性能分析:
- 最优情况下为完全二叉树,查找的时间复杂度为O(logN);
- 最坏情况下为链表,查找的时间复杂度为O(N).
三>Oj题练习:
1.宝石与坏石头:
class Solution { public int numJewelsInStones(String jewels, String stones) { HashSet<Character> set = new HashSet<>(); int count = 0; for(int i = 0;i<jewels.length();i++){ char ch = jewels.charAt(i); set.add(ch); } for(int i = 0;i<stones.length();i++){ char ch = stones.charAt(i); if(set.contains(ch)){ count++; } } return count; } }
2.复制带随机指针的链表:(百度电话面试)
- 该题题目较为难懂, 大致意思是给定一个链表,该链表有值域.指针域.和随机指针域,要求复制一份同样的链表但只保留原来链表的形式,其中指针域的数据全部改变.题目较为友好的是原链表中的随机指针只会指向原链表的其他节点或者null,所以不必担心新链表的随机指针无法配对.
- 该题核心思路是使用HashMap建立原链表和新链表的映射关系.
- 解题步骤:定义一个临时变量cur指向原链表的头结点,cur每遍历一个链表的节点就创建一个新节点然后用HashMap记录原链表与新链表的关系,此时新链表是一个个分散的节点.cur再次指向头结点遍历原链表,map.get(key)代表新节点的地址,hashMap.get(cur).next = hashMap.get(cur.next);hashMap.get(cur).random = hashMap.get(cur.random);
class Solution { public Node copyRandomList(Node head) { HashMap<Node,Node> hashMap = new HashMap<>(); //遍历原来的链表存储对应关系 Node cur = head; while(cur!=null){ Node node = new Node(cur.val); hashMap.put(cur,node); cur = cur.next; } cur = head; while(cur!=null){ hashMap.get(cur).next = hashMap.get(cur.next); hashMap.get(cur).random = hashMap.get(cur.random); cur = cur.next; } return hashMap.get(head); } }
3.坏键盘打字:
import java.util.*; public class Main{ public static void func1(String str1,String str2){ HashSet<Character> set = new HashSet<>(); for(char ch:str2.toUpperCase().toCharArray()){ set.add(ch); } for(char ch:str1.toUpperCase().toCharArray()){ if(!set.contains(ch)){ System.out.print(ch); set.add(ch); } } } public static void main(String[] args){ Scanner scan = new Scanner(System.in); while(scan.hasNextLine()){ //应输入的字 String str1 = scan.nextLine(); //实际输入的字 String str2 = scan.nextLine(); func1(str1,str2); } } }
4.前K个高频单词:
public List<String> topKFrequent(String[] words, int k) { HashMap<String,Integer> map = new HashMap<>(); //1.遍历word数组统计字符出现的次数 for(String word:words){ if(map.get(word)==null){ map.put(word,1); }else{ Integer val = map.get(word); map.put(word,val+1); } } //单词统计完毕 //2.遍历map中的每个Entry,建立大小为K的小根堆 PriorityQueue<Map.Entry<String, Integer>> minHeap = new PriorityQueue<>(k, new Comparator<Map.Entry<String, Integer>>() { @Override public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { if (o1.getValue().compareTo(o2.getValue())==0){ return o2.getKey().compareTo(o1.getKey()); } return o1.getValue().compareTo(o2.getValue()); } }); //3.遍历map中的每个Entry for (Map.Entry<String, Integer> entry : map.entrySet()) { if (minHeap.size() < k) { minHeap.offer((entry)); } else { Map.Entry<String, Integer> top = minHeap.peek(); //出堆 if (entry.getValue().compareTo(top.getValue()) > 0) { minHeap.poll(); minHeap.offer(entry); } else { //频率相同 if (entry.getValue().compareTo(top.getValue()) == 0) { if (top.getKey().compareTo(entry.getKey()) > 0) { minHeap.poll(); minHeap.offer(entry); } else { } } } } } List<String> ret = new ArrayList<>(); for (int i = 0;i<k;i++){ Map.Entry<String,Integer> top = minHeap.poll(); ret.add(top.getKey()); } Collections.reverse(ret); return ret; }