算法题之hash相关的题目

64 阅读12分钟

1.Hash基础

1.1Hash的概念和基本特征

哈希(Hash)也称为散列,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,这个输出值

就是散列值。

很多人可能想不明白,这里的映射到底是啥意思,为啥访问的时间复杂度为O(1)?我们只要看存的时候和

读的时候分别怎么映射的就知道了。

我们现在假设数组array存放的是1到15这些数,现在要存在一个大小是7的Hash表中,该如何存呢?我们

存储的位置计算公式是:

index=number&7

通过取模运算。然后继续存7-13,结果如下

最后再存 14和15

这时候我们会发现有些数据被存到同一个位置了,我们后面再讨论。接下来,我们看看如何取。

假如我要测试13在不在这里结构里,则同样使用上面的公式来进行,很明显13模7=6,我们直接访问

array[6]这个位置,很明显是在的,所以返回true。

假如我要测试20在不在这里结构里,则同样使用上面的公式来进行,很明显20模7=6,我们直接访问

array[6]这个位置,但是只有6和13,所以返回false。.

理解这个例子我们就理解了H©s是如何进行最基本的映射的,还有就是为什么访问的时间复杂度为O(1)。

1.2Hash碰撞的处理方法

在上面的例子中,我们发现有些在Hsh中很多位置可能要存两个甚至多个元素,很明显单纯的数组是不行

的,这种两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫故碰撞。

那该怎么解决呢?常见的方法有:开放定址法(Uava里的Threadlocal、链地址法(Uava里的

ConcurrentHashMap)、再哈希法(布隆过滤器)、建立公共溢出区。后两种用的比较少,我们重点看前两

个。

开放地址法:(也就是扩大数组大小)

开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能

找到,并将记录存入。

链地址法(也就是数组存放的元素改为链式结构)

在HashMap(JDK8之前)底层就是这样实现的

将哈希表的每个单元作为链表的头结点,所有哈希地址为的元素构成一个同义词链表。即发生冲突时就把

该关键字链在以该单元为头结点的链表的尾部。例如:

队列的基础知识

2.1队列的概念和基本特征

基本特征,就是存数据是一个端口,取数据是一个端口,也就是一个存取受限的线性表结构

即FIFO先进先出原则

队列的实现也有两种方法:基于数据/基于链表

下面我们采用链表来实现队列

2.2实现队列

采用双向链表实现

public class Node {
   public int val;
   public Node next;
   public Node pre;

   public Node() {
   }

   public Node(int val) {
      this.val = val;
   }
}
public class LinkQueue {
   public static void main(String[] args) {
      LinkQueue linkQueue=new LinkQueue();
      linkQueue.push(1);
      linkQueue.push(2);
      linkQueue.push(3);
      System.out.println(linkQueue.pull());
      linkQueue.traverse();
   }
   private Node front;
   private Node rear;
   private int size;

   public LinkQueue(){
      front=null;
      rear=null;
      size=0;
   }
   //入队
   public void push(int val){
      Node node=new Node(val);
      if(front==null&&rear==null){
         front=node;
         rear=node;
      }else {
         node.next=rear;
         rear.pre=node;
         rear=node;
      }
   }
   //出队
   public int pull(){
      if(front==null&&rear==null){
         throw  new RuntimeException("队列为空");
      }
      if(front==rear){
         int res=front.val;
         front=null;
         rear=null;
         return res;
      }
      int res=front.val;
      Node node=front.pre;
      node.next=null;
      front=node;
      return res;
   }
   //遍历队列
   public void traverse(){
      Node cur=front;
      while (cur!=null){
         System.out.println(cur.val);
         cur=cur.pre;
      }
   }
}

1. 用栈实现队列

用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

解题思路:

队列的特性是 FIFOFIFOFIFO(先入先出),而栈的特性是 FILOFILOFILO(先入后出)。

知道两者特性之后,我们需要用两个栈来模拟队列的特性,一个栈为入队栈,一个栈为出对栈。

当出队栈存在内容时,出队栈的栈顶,即为第一个出队的元素。

若出队栈无元素,我们的需求又是出队的话,我们就需要将入队栈的内容反序导入出队栈,然后弹出栈顶即可。

注意:根据栈的的特性,我们仅能使用 pushpushpush 和 poppoppop 操作。

class MyQueue {
    private static Stack<Integer> inStack;
    private static Stack<Integer> outStack;

    public MyQueue() {
        inStack=new Stack<Integer>();
        outStack=new Stack<Integer>();

    }
    
    public void push(int x) {
        inStack.push(x);

    }
    
    public int pop() {
        if(outStack.isEmpty()){
            in2out();
        }
        return outStack.pop();

    }

    private void in2out() {
        while (!inStack.isEmpty()){
            Integer pop = inStack.pop();
            outStack.push(pop);
        }
    }

    public int peek() {
        if(outStack.isEmpty()){
            in2out();
        }
        return outStack.peek();

    }
    
    public boolean empty() {
        if(inStack.isEmpty()&&outStack.isEmpty()){
            return true;
        }else {
            return false;
        }

    }
}

2. 用队列实现栈

用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

解题思路:两个队列

为了满足栈的特性,即最后入栈的元素最先出栈,在使用队列实现栈时,应满足队列前端的元素是最

后入栈的元素。可以使用两个队列实现栈的操作,其中queue1用于存储栈内的元素,queve2作为入

栈操作的辅助队列。

入栈操作时,首先将元素入队到queue2,然后将queue1的全部元素依次出队并入队到queue2,,此

时queue2的前端的元素即为新入栈的元素,再将queue1和queue2互换,则queue1的元素即为栈

内的元素,queue1的前端和后端分别对应栈顶和栈底。

由于每次入栈操作都确保guue1的前瑞元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以

简单实现。出栈操作只需要移除quue1的前端元素并返回即可,获得栈顶元素操作只需要获得

queue1的前端元素并返回即可(不移除元素)。

由于queue1用于存储栈内的元素,判断栈是否为空时,只需要判断queue1是否为空即可。

class MyStack {
    private Queue<Integer> queue1;
    private Queue<Integer> queue2;


    public MyStack() {
        queue1=new LinkedList<>();
        queue2=new LinkedList<>();

    }
    
    public void push(int x) {
        queue2.offer(x);
        while (!queue1.isEmpty()){
            queue2.offer(queue1.poll());
        }
        Queue<Integer> temp=queue1;
        queue1=queue2;
        queue2=temp;

    }
    
    public int pop() {
        return queue1.poll();

    }
    
    public int top() {
        return queue1.peek();

    }
    
    public boolean empty() {
            return queue1.isEmpty();
    }
}

一个队列:

入栈操作时,首先获得入栈前的元素个数 nnn,然后将元素入队到队列,再将队列中的前 nnn 个元素(即除了新入栈的元素之外的全部元素)依次出队并入队到队列,此时队列的前端的元素即为新入栈的元素,且队列的前端和后端分别对应栈顶和栈底。

由于每次入栈操作都确保队列的前端元素为栈顶元素,因此出栈操作和获得栈顶元素操作都可以简单实现。出栈操作只需要移除队列的前端元素并返回即可,获得栈顶元素操作只需要获得队列的前端元素并返回即可(不移除元素)。



class MyStack{
    private Queue<Integer> queue;



    public MyStack() {
        this.queue=new LinkedList<Integer>();


    }
    
    public void push(int x) {
        int size = queue.size();
        queue.offer(x);
        while (size>0){
            queue.offer(queue.poll());
            size--;
        }

    }
    
    public int pop() {
        return queue.poll();

    }
    
    public int top() {
        return queue.peek();

    }
    
    public boolean empty() {
            return queue.isEmpty();
    }
}

3. n数之和专题

3.1. 两数之和

解题思路:

Hashmap:每次存入 nums[i] - i

如果遍历的时候 map.contains(target-nums[i],就将 value 和i返回

class Solution {
    public int[] twoSum(int[] nums, int target) {
        HashMap<Integer,Integer> map=new HashMap<>();
        int[] res=new int[2];
        for (int i = 0; i < nums.length; i++) {
            if(map.containsKey(target-nums[i])){
                res[0]=map.get(target-nums[i]);
                res[1]=i;
                break;
            }else {
                map.put(nums[i],i);
            }
        }
        return res;
    }
}

3.2. 三数之和

三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。

解题思路:

排序+双指针+去重

class Solution {
    public static void main(String[] args) {
        int[] nums={-1,0,1,2,-1,-4};
        threeSum(nums);
    }
    public static List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> res=new ArrayList<>();
        for (int i = 0; i < nums.length-2; i++) {
            int x=nums[i];
            int l=1+i;
            int r=nums.length-1;
            while (l<r){
                if(nums[l]+nums[r]+x==0){
                    List<Integer> list=Arrays.asList(x,nums[l],nums[r]);
                    res.add(list);
                    l++;
                    r--;
                }else if(nums[l]+nums[r]+x>0){
                    r--;
                }else {
                    l++;
                }
            }
        }

        return res.stream().distinct().collect(Collectors.toList());

    }
}

LRU也是非常经典的问题,而且常年是算法的热门,其实现也有技巧,现在我们就来一起看看。

缓存是应用软件的必备功能之一,在操作系统,Java里的Spring、mybatis、redis、mysql等软件中都有

自己的内部缓存模块,而缓存是如何实现的呢?在操作系统教科书里我们知道常用的有FFO、LRU和LFU

三种基本的方式。FIFO也就是队列方式不能很好利用程序局部性特征,缓存效果比较差,一般使用LRU

(最近最少使用)和LFU(最不经常使用淘汰算法)比较多一些。LRU是淘汰最长时间没有被使用的页面,

而LU是淘汰一段时间内,使用次数最少的页面。

从实现上LRU是相对容易的,而LFU比较复杂,我们本章重点研究一下LRU的问题,这也是一道高频题

目。LeetCode146:设计一个LRU缓存,这个题也经常见到,在牛客也是长期排名前三

1. LRU含义

先看看leetcode146,设计和实现一个LRU缓存机制

LRU

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

一个不太好的思路:

一个map存数据

一个map存(key,存入的时间)

class LRUCache {
    private HashMap<Integer,Integer> map;

    private HashMap<Integer,Integer> mapLRU;
    int size;
    int time;

    public LRUCache(int capacity) {
        map=new HashMap<>();

        mapLRU=new HashMap<>();
        size=capacity;
        time=0;
    }
    
    public int get(int key) {
        Integer res = map.get(key);
        if(res==null){
            return -1;
        }
        mapLRU.put(key, time++);
        return res;
    }
    
    public void put(int key, int value) {
        if(map.size()==size&&!map.containsKey(key)){
            Set<Integer> set = mapLRU.keySet();
            int min=Integer.MAX_VALUE;
            int myKey=-1;
            for (Integer i : set) {
                Integer num = mapLRU.get(i);
                if(num<min){
                    min=num;
                    myKey=i;
                }
            }
            mapLRU.remove(myKey);
            map.remove(myKey);
        }
        map.put(key,value);
        mapLRU.put(key,time++);

    }
}

哈希表+双向链表

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。

哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1)O(1)O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:

对于 get 操作,首先判断 key 是否存在:

如果 key 不存在,则返回 −1;

如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。

对于 put 操作,首先判断 key 是否存在:

如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;

如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

public class LRUCache {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}