您的当前位置:首页数据结构算法(十一) 之 散列表查找(哈希表)

数据结构算法(十一) 之 散列表查找(哈希表)

2024-12-14 来源:哗拓教育

一、散列函数构造方法

  • 除留取余法

对于散列表长度为 m 的散列函数公式为:

f(key)= key mod p (p <= m)

mod 就是取余的方法。一般 p 为小于或等于表长(最好接近 m)的最小质数或者不包含小于 20 质因子的合数。

二、处理散列冲突的方法

  • 1. 开放地址法

一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

线性探测法:
fi(key)= (f(key)+ di)MOD m (di = 1,2,3,......,m-1)

  • 2. 链地址法

其实这个就是 HashMap 的实现原理,将所有关键字为同义词的记录存储在一个单链表中,散列表只需要存储所有同义词子表的头指针就行了。

三、查找的一些面试题

  • 剑指 Offer 面试题 3(Java 版):二维数组中的查找

题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路: 首先选取数组中右上角的数字。如果该数字等于要查找的数字,查找过程结束;如果该数字大于要查找的数字,剔除这个数字所在的列;如果该数字小于要查找的数字,剔除这个数字所在的行。

也就是说如果要查找的数字不在数组的右上角,则每一次都在数组的查找范围中剔除)行或者一列,这样每一步都可以缩小查找的范围,直到找到要查找的数字,或者查找范围为空。

show my code

/**
 * 二维数组的查找
 * @author innovator
 *
 */
public class BinaryArraySearch {

     /**
     * 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。
     * 请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
     * <p/>
     * 规律:首先选取数组中右上角的数字。如果该数字等于要查找的数字,查找过程结束:
     * 如果该数字大于要查找的数字,剔除这个数字所在的列:如果该数字小于要查找的数字,剔除这个数字所在的行。
     * 也就是说如果要查找的数字不在数组的右上角,则每-次都在数组的查找范围中剔除)行或者一列,这样每一步都可以缩小
     * 查找的范围,直到找到要查找的数字,或者查找范围为空。
     *
     * @param data 待查找的数组
     * @param number 要查找的数
     * @return 查找结果,true 找到,false 没有找到
     */
    public static boolean find(int[][] data,int number){
        
        boolean found = false;
        
        //输入条件判断
        if(data == null || data.length <1 || data[0].length <1){
            return false;
        }
        
        //选取右上角的数字
        int row = 0;
        int colum = data[0].length-1;
        
        //行的数量
        int rows = data.length;
        //列的数量 
        int colums = data[0].length;
        
        //行数正向增加,列数减少,同时保证在数组内
        while(row >=0 && row < rows && colum >=0 && colum < colums){
            //右上角的数字和指定的数字相等
            if(data[row][colum] == number){
                found = true;
                break;
            }else if(data[row][colum] > number){
                //右上角的数字大于指定的数字,可以去掉最后一列
                colum --;
            }else{
                //右上角的数字小于指定的数字,可以去掉所在的这一行
                row ++;
            }
        }
        
        return found;
    }
    
    public static void main(String[] args){
        
        int[][] matrix = {
                {1, 2, 8, 9},
                {2, 4, 9, 12},
                {4, 7, 10, 13},
                {6, 8, 11, 15}
        };
        System.out.println(find(matrix, 7));    // 要查找的数在数组中
        System.out.println(find(matrix, 5));    // 要查找的数不在数组中
        System.out.println(find(matrix, 1));    // 要查找的数是数组中最小的数字
        System.out.println(find(matrix, 15));   // 要查找的数是数组中最大的数字
        System.out.println(find(matrix, 0));    // 要查找的数比数组中最小的数字还小
        System.out.println(find(matrix, 16));   // 要查找的数比数组中最大的数字还大
        System.out.println(find(null, 16));     // 健壮性测试,输入空指针
        
    }
}
结果
  • 剑指 Offer 面试题 35(Java 版):第一个只出现一次的字符

题目:在字符串中找出第一个只出现一次的字符。如输入 "abaccdef",则输出 'b'。

思路: 我们可以通过将扫描到的 char 当成哈希表的 key,它出现的次数作为 value。当我们扫描完一次后,所有字符出现的次数都已经检查完毕了,接下来只需要顺序输出哈希表的 Entry,如果出现的次数是 1,那么直接返回。整个过程的时间复杂度是 O(n) 和 O(1)。

show my code

/**
 * 寻找第一个不重复出现的字符
 * @author innovator
 *
 */
public class FirstNotRepeatChar {

    /**
     * 寻找第一个不重复出现的字符
     * @param s
     * @return
     */
    public static char findFirstNotRepeatChar(String s){
        
        if(s == null || s.length() <1){
            throw new IllegalArgumentException("String should not be null or empty");
        }
        
        //用 LinkedHashMap 存放每个字符出现的次数,同时保证扫描顺序
        Map<Character,Integer> times = new LinkedHashMap();
        //第一次扫描字符串
        for(int i=0;i<s.length();i++){
            char c = s.charAt(i);
            
            //不包含
            if(!times.containsKey(c)){
                times.put(c, 1);
            }else{
                int value = times.get(c);
                times.put(c, value+1);
            }
        }
        
        Character result = '\0';
        //获取所有扫描的数据
        Set<Map.Entry<Character, Integer>> entries = times.entrySet();
        for(Map.Entry<Character, Integer> entry:entries){
            //只出现过一次
            if(entry.getValue() == 1){
                result = entry.getKey();
                return result;
            }
        }
        
        return result;
        
    }
    
    public static void main(String[] args) {
        System.out.println(findFirstNotRepeatChar("google")); // l
        System.out.println(findFirstNotRepeatChar("aabccdbd")); // '\0'
        System.out.println(findFirstNotRepeatChar("abcdefg")); // a
        System.out.println(findFirstNotRepeatChar("gfedcba")); // g
        System.out.println(findFirstNotRepeatChar("zzgfedcba")); // g
    }
}
结果
  • 剑指 Offer 面试题 40(Java 版):数组中只出现一次的数字

题目:一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

思路: 这个题目在强调一个(或两个)数字只出现一次,其他的出现两次。这有什么意义呢?我们想到异或运算的一个性质:任何一个数字异或它自己都等于0。了解了异或去重的原理,而且知道如果只有一个只出现一次的数字的求法,我们便要想办法把它分为两个子数组,每个子数组中包含一个只出现一次的数字,其他的数字都出现了两次。

剑指 offer 上的思路很巧妙,依然从头到尾异或所有的数字,这样得到的结果实际上就是两个只出现了一次的数字异或的结果,我们在异或后的结果中找出其二进制中最右边为 1 的位,该位既然为 1,说明异或的两个数 字对应的该位肯定不同,必定一个为 1,一个为 0。因此我们可以考虑根据此位是否为 1 来划分这两个子数组,这样两个只出现一次的数字就分开了。

但我们还要保证出现两次的数字都分到同一个子数组中,肯定不能两个重复的数字分在两个不同的子数组中,这样得到的结果是不对的。很明显,相同的数字相同的位上的值是相同的,要么都为 1,要么都为 0,因此我们同样可以通过判断该位是否为 1 来将这些出现两次的数字划分到同一个子数组中,该位如果为 1,就分到一个子数组中,如果为 0,就分到另一个子数组中。这样就能保证每个子数组中只有一个出现一次的数字,其他的数字都出现两次,分别全部异或即可得到这两个只出现一次的数字。时间复杂度为 O(n)。

另外,所有元素异或后,在找出最右边为 1 的时,我用的比剑指 offer 上更简洁的代码,主要用到了下面的结论:

对于一个数字 X,X&(-X) 之后得到的数字,是把 X 中最右边的 1 保留下来,其他位全部为 0。注意,这里的 -X 是 X 的相反数,-X=~X+1,这里的 ~X 意思是对 X 所有位取反,不要将二者弄混了。

show my code

public class FindNumsAppearOnceTest {

    /**
     * 寻找只出现一次的数字
     * @param arr
     * @throws Exception 
     */
    public static void findNumsAppearOnce(int[] arr) throws Exception{
        
        if(arr == null || arr.length<2){
            throw new Exception("输入的数据不合法");
        }
        
        int i;
        int AllXOR = 0;
        
        //全部异或
        for(i=0;i<arr.length;i++){
            
            AllXOR ^= arr[i];
        }
        

        //0x0001,找到最右边第一位为 1 的数
        int res = findFirstBit1(AllXOR);

        int num1 = 0;
        int num2 = 0;
        
        for(int k=0;k<arr.length;k++){
            
            //同一种类型
            if(isBit1(arr[k],res)){
                num1 ^= arr[k];
            }else {
                num2 ^= arr[k];
            }   
        }
        
        System.out.println("不重复的数字:"+num1);
        System.out.println("不重复的数字:"+num2);
    }
    
    /**
     * 返回 num 的最低位的 1,其他各位都为 0
     * 
     * 对于一个数字X,X&(-X)之后得到的数字,是把X中最右边的1保留下来,其他位全部为0。注意,
     * 这里的-X是X的相反数,-X=~X+1,这里的~X意思是对X所有位取反,不要将二者弄混了。
     * @param num
     * @return
     */
    public static int findFirstBit1(int num){
        
        //二者与后得到的数,将 num 最右边的 1 保留下来,其他位的全部置为了 0
        return num & (-num);
    }
    
    
    /**
     * 判断 num 中特定的位是否为 1,这里的要判断的特定的位由 res 确定,res 中只有一位为 1,
     * 其他位均为 0,由 findFirstBit1 函数返回,而 num 中要判断的位便是 res 中这唯一的 1 所在的位
     * 
     * 根据这个来将数组分成两个数组,保证每个数组只有一个数字只出现一次
     * @param num
     * @param res
     * @return
     */
    public static boolean isBit1(int num,int res){
        
        return ((num & res)==0) ? false:true;
    }
    
    
    public static void main(String[] args) throws Exception {
        int[] data = {
                2,4,3,6,3,2,5,5
        };
        
        findNumsAppearOnce(data);
    }
    
}
结果
显示全文