数据结构与算法四:查找算法&&哈希表

一、查找算法

1 查找算法的介绍

  • 在java中,我们常用的查找算法有四种
  1. 顺序(线性)查找
  2. 二分法查找
  3. 插值查找
  4. 斐波那契查找

2 线性查找算法

有一个数列: {1,8, 10, 89, 1000, 1234} ,判断数列中是否包含此名称【顺序查找】 要求: 如果找到了,就提示找到,并给出下标值。
思路:如果查找到全部符合条件的值。[思路分析.]

/**
 * @author Wnlife
 * @create 2019-11-21 9:20
 *
 * 顺序查找算法
 */
public class SeqSearch {

    public static void main(String[] args) {

        int[]arr={1,36,8,6,4,6,2,56,2,54,6};
        int r = seqSearch(arr, 2);
        if(r==-1){
            System.out.println("没有找到!");
        }else {
            System.out.println("找到的下标为:"+r);
        }
    }

    public static int seqSearch(int[]arr,int key){
        for (int i = 0; i < arr.length; i++) {
            if(arr[i]==key)
                return i;
        }
        return -1;
    }
}

3 二分法查找算法

3.1 实例分析

请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。

3.2 图解分析

在这里插入图片描述

3.3 二分查找的代码

/**
 * @author Wnlife
 * @create 2019-11-21 9:28
 * <p>
 * 二分查找法
 */
public class BinSearch {

    public static void main(String[] args) {

        int[] arr = {1, 8, 10, 89, 1000, 1234};
//        int index = binarySearch1(arr, 0, arr.length-1, 89);
        int index = binarySearch2(arr, 89);
        System.out.println(index);
    }

    /**
     * 递归版的二分法查找
     *
     * @param arr   要查找的数组
     * @param left  数组左边的值
     * @param right 数组右边的值
     * @param key   要查找的元素
     * @return 查找到的值
     */
    public static int binarySearch1(int[] arr, int left, int right, int key) {

        //表示找不到
        if (left > right) {
            return -1;
        }
        //递归查找
        int mid = (left + right) / 2;
        if (arr[mid] > key) {
            return binarySearch1(arr, left, mid - 1, key);
        } else if (arr[mid] < key) {
            return binarySearch1(arr, mid + 1, right, key);
        } else {
            return mid;
        }
    }

    /**
     * 循环版的二分法查找
     *
     * @param arr
     * @param key
     * @return
     */
    public static int binarySearch2(int[] arr, int key) {
        int left = 0;
        int right = arr.length - 1;
        while (left <= right) {
            int mid = (left + right) >> 1;
            if (arr[mid] > key) {
                right = mid - 1;
            } else if (arr[mid] < key) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }


}

3.4 课后思考题

{1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000.

代码:

    /*
     * 完成一个课后思考题:
     *
     * 课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,
     * 有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000
     *
     * 思路分析
     * 1. 在找到mid 索引值,不要马上返回
     * 2. 向mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList
     * 3. 向mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList
     * 4. 将Arraylist返回
     */
    public static List<Integer> binarySearch3(int[] arr, int left, int right, int key) {

        //没找到
        if (left > right)
            return new ArrayList<Integer>();
        //递归查找
        int mid = (left + right) >> 1;
        if (key < arr[mid]) {
            return binarySearch3(arr, left, mid - 1, key);
        } else if (key > arr[mid]) {
            return binarySearch3(arr, mid + 1, right, key);
        } else {
            // 1. 在找到mid 索引值,不要马上返回
            List<Integer> list = new ArrayList<Integer>();
            // 2. 向mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList
            int temp = mid - 1;
            while (true) {
                if (temp < left || arr[temp] != key) {
                    break;
                }
                list.add(temp);
            }
            list.add(mid);
            // 3. 向mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList
            temp = mid + 1;
            while (true) {
                if (temp > right || arr[temp] != key) {
                    break;
                }
                list.add(temp);
            }
            // 4. 将Arraylist返回
            return list;
        }
    }

4 插值查找算法

4.1 插值查找算法的原理介绍:

  1. int[] arr = {1, 8, 10, 89, 1000, 1234};
  2. 将折半查找中的求mid 索引的公式 , low 表示左边索引left, high表示右边索引right;
    在这里插入图片描述
  3. int mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;/插值索引/
    对应前面的代码公式:
    int mid = left + (right – left) * (key– arr[left]) / (arr[right] – arr[left])

代码实现:

/**
 * @author Wnlife
 * @create 2019-11-21 12:13
 *
 * 插值查找算法
 */
public class InsertSearch {

    public static void main(String[] args) {
        int[] arr = {1, 8, 10, 89, 1000, 1234};
        int index = insertSearch(arr, 0, arr.length - 1, 10);
        System.out.println(index);
    }

    /**
     * 说明:插值查找算法,也要求数组是有序的
     * @param arr 数组
     * @param left 坐标索引
     * @param right 右边索引
     * @param key 查找的值
     * @return 如果找到,就返回对应的下标,如果没有找到,返回-1
     */
    public static int insertSearch(int[]arr,int left,int right,int key){

        //注意:findVal < arr[0]  和  findVal > arr[arr.length - 1] 必须需要
        //否则我们得到的 mid 可能越界
        if(left>right||key<arr[left]||key>arr[right]){
            return -1;
        }
        // 求出mid, 自适应
        int mid=left+(right-left)*(key-arr[left])/(arr[right]-arr[left]);
        if(key<arr[mid]){
            return insertSearch(arr,left,mid-1,key);
        }else if(key>arr[mid]){
            return insertSearch(arr,mid+1,right,key);
        }else{
            return mid;
        }
    }
}

4.2 插值查找注意事项

  1. 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
  2. 关键字分布不均匀的情况下,该方法不一定比折半查找要好

5 斐波那契(黄金分割法)查找算法

5.1 斐波那契(黄金分割法)查找基本介绍

  1. 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
  2. 斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55 } 发现斐波那契数列的两个相邻数 的比例,无限接近 黄金分割值0.618

5.2 斐波那契(黄金分割法)原理:

斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近,即mid=low+F(k-1)-1(F代表斐波那契数列),如下图所示:
在这里插入图片描述
对F(k-1)-1的理解:

  1. 由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1 。该式说明:只要顺序表的长度为F[k]-1,则可以将该表分成长度为F[k-1]-1和F[k-2]-1的两段,即如上图所示。从而中间位置为mid=low+F(k-1)-1
  2. 类似的,每一子段也可以用相同的方式分割
  3. 但顺序表长度n不一定刚好等于F[k]-1,所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能使得F[k]-1恰好大于或等于n即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1F[k]-1位置),都赋为n位置的值即可。
       while(n>fib(k)-1){
           k++;
       }

5.3 斐波那契查找应用案例:

请对一个有序数组进行斐波那契查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。

代码:

/**
 * @author Wnlife
 * @create 2019-11-21 16:12
 * <p>
 * 斐波那契(黄金分割查找)查找f
 */
public class FibonacciSearch {

    public static int maxSize = 20;
    public static void main(String[] args) {
        int[] arr = {1, 8, 10, 89, 1000, 1234};
        System.out.println("index=" + fibonacciSearch(arr, 1234));// 0
    }

    /**
     * 创建斐波那契数列
     *
     * @return 斐波那契数列
     */
    public static int[] fib() {
        int[] fib = new int[maxSize];
        fib[0] = 1;
        fib[1] = 1;
        for (int i = 2; i < maxSize; i++) {
            fib[i] = fib[i - 1] + fib[i - 2];
        }
        return fib;
    }

    /**
     * 使用非递归的方式编写斐波那契查找算法
     *
     * @param arr 查找的数组
     * @param key 要查找的值
     * @return 查找到的数组索引,找到返回角标,找不到返回-1
     */
    public static int fibonacciSearch(int[] arr, int key) {

        int left = 0;
        int right = arr.length - 1;
        int k = 0;//表示斐波那契数列的下标
        int mid = 0;
        int f[] = fib();//获取的斐波那契数列

        //计算斐波那契数列的下标
        while (arr.length > f[k] - 1) {
            k++;
        }
        //原数组的长度n不一定刚好等于f[k]-1,所以需要将原来的顺序表长度n增加至f[k]-1
        //因此需要重新创建一个数组,将原数组分值复制过去
        int[] temp = Arrays.copyOf(arr, f[k] - 1);
        //新增的位置(从n+1到f[k]-1位置),都赋为n位置的值即可
        for (int i = right + 1; i < temp.length; i++) {
            temp[i] = arr[right];
        }

        //查找对应值的角标
        while (left <= right) {
            //由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,
            //可以得到 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1 。
            // 该式说明:只要顺序表的长度为F[k]-1,则可以将该表分成长度为F[k-1]-1和F[k-2]-1的两段,
            //即如上图所示。从而中间位置为mid=low+F(k-1)-1
            //现在数组的长度已经变为f[k]-1,则可以使用上述理论
            mid = left + f[k - 1] - 1;
            if (key < temp[mid]) {//需要在数组的左边查找,即:在(F[k-1]-1)这边找
                right = mid - 1;
                //对应的斐波那契数列的为F[k-1],k需要减1
                k -= 1;
            } else if (key > temp[mid]) {//需要在数组的右边查找,即:在(F[k-1]-1)这边找
                left = mid + 1;
                //对应的斐波那契数列的为F[k-2],k需要减2
                k -= 2;
            } else {//找到对应的元素
                //需要判断返回哪个下标,因为目前使用的是增长后的临时数组
                if (mid <= right) {
                    return mid;
                } else {
                    return right;
                }
            }
        }
        return -1;
    }
    
}

二、哈希表

2.1哈希表(散列)

哈希表的基本介绍
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
在这里插入图片描述

2.2google公司的一个上机题:

有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,名字,住址…),当输入该员工的id时,要求查找到该员工的 所有信息.
要求:
不使用数据库,速度越快越好=>哈希表(散列)
添加时,保证按照id从低到高插入 [课后思考:如果id不是从低到高插入,但要求各条链表仍是从低到高,怎么解决?]

  1. 使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
  2. 思路分析并画出示意图
  3. 代码实现[增删改查(显示所有员工,按id查询)]

代码实现:

/**
 * @author Wnlife
 * @create 2019-11-21 19:25
 * <p>
 * 哈希表的实现
 */
public class HashTabDemo {

    public static void main(String[] args) {

        HashTab hashTab = new HashTab(7);
        Scanner in = new Scanner(System.in);
        String key = null;
        while (true) {
            System.out.println("add: 添加雇员");
            System.out.println("list: 显示雇员");
            System.out.println("find: 查找雇员");
            System.out.println("delete: 删除雇员");
            System.out.println("exit: 退出系统");
            key = in.next();
            switch (key) {
                case "add": {
                    System.out.println("请输入雇员的ID");
                    int id = in.nextInt();
                    System.out.println("请输入雇员的姓名");
                    String name = in.next();
                    //创建雇员
                    Employee employee = new Employee(id, name);
                    hashTab.add(employee);
                    break;
                }
                case "list": {
                    hashTab.list();
                    break;
                }
                case "find": {
                    System.out.println("请输入雇员的ID:");
                    int id = in.nextInt();
                    hashTab.findById(id);
                    break;
                }
                case "delete": {
                    System.out.println("请输入雇员的ID:");
                    int id = in.nextInt();
                    hashTab.deleteById(id);
                    break;
                }
                case "exit":
                    in.close();
                    System.exit(0);
            }
        }
    }
}

/**
 * 表示每个员工,也是链表中的一个节点
 */
class Employee {

    public int id;
    public String name;
    public Employee next;

    public Employee(int id, String name) {
        this.id = id;
        this.name = name;
    }
}


/**
 * 表示哈希表中的每个链表
 */
class EmployeeLinkedList {

    /*链表头指针,直接指向链表的第一个节点*/
    private Employee head;

    /**
     * 添加节点到链表
     * 说明:
     * 1.假定,当添加雇员时,id 是自增长,即id的分配总是从小到大
     * 因此我们将该雇员直接加入到本链表的最后即可
     *
     * @param employee 要添加的节点
     */
    public void add(Employee employee) {
        /*如果添加的是第一个雇员*/
        if (head == null) {
            head = employee;
        } else {
            /*如果添加的不是第一个雇员,将新雇员添加到链表的最后*/
            Employee temp = head;
            while (temp.next != null) {
                temp = temp.next;
            }
            temp.next = employee;
        }
    }

    /**
     * 遍历链表的雇员信息
     *
     * @param no 第几个链表,链表编号
     */
    public void list(int no) {

        if (head == null) {
            System.out.println("第 " + (no + 1) + " 链表为空");
            return;
        }
        Employee temp = head;
        System.out.printf("第%d条链表的信息为:", no + 1);
        while (temp != null) {
            System.out.printf(" id=%d name=%s\t", temp.id, temp.name);
            temp = temp.next;
        }
        System.out.println();
    }

    /**
     * 根据ID查找雇员
     *
     * @param id 要查找的雇员ID
     * @return 范围雇员的对象
     */
    public Employee findByID(int id) {
        if (head == null) {
            System.out.println("链表为空~~");
            return null;
        }
        Employee temp = head;
        while (temp != null) {
            if (temp.id == id) {
                return temp;
            }
            temp = temp.next;
        }
        /*没有找到*/
        return null;
    }

    /**
     * 根据员工的ID在哈希表里面删除员工
     *
     * @param id 员工的ID
     */
    public void deleteById(int id) {

        if (head == null) {
            System.out.println("这个员工不存在~~");
            return;
        }
        /*如果删除的是头节点*/
        if (head.id == id) {
            head = head.next;
            return;
        }
        /*如果删除的不是头结点*/
        Employee temp = head;
        while (temp.next != null) {
            if (temp.next.id == id) {
                temp.next = temp.next.next;
                System.out.println("Id为" + id + "的员工已经被删除~~");
                return;
            }
        }
        System.out.println("没有找到这个员工~~");
    }

}

/**
 * 创建哈希表管理多条链表
 */
class HashTab {

    /*链表数组*/
    private EmployeeLinkedList[] employeeLinkedLists;

    /*链表数组的大小*/
    int size;

    /*构造函数*/
    public HashTab(int size) {
        this.size = size;
        employeeLinkedLists = new EmployeeLinkedList[size];
        for (int i = 0; i < employeeLinkedLists.length; i++) {
            employeeLinkedLists[i] = new EmployeeLinkedList();
        }
    }

    /**
     * 根据员工的ID计算链表数组的索引值的方法
     *
     * @param id 员工的ID
     * @return 链表数组的索引值
     */
    public int getIndex(int id) {
        return id % size;
    }

    /*添加节点的方法*/
    public void add(Employee employee) {
        int index = getIndex(employee.id);
        employeeLinkedLists[index].add(employee);

    }

    /*遍历链表节点的方法*/
    public void list() {
        for (int i = 0; i < size; i++) {
            employeeLinkedLists[i].list(i);
        }
    }

    /**
     * 根据雇员的ID在哈希表中查找雇员
     *
     * @param id 雇员的Id
     */
    public void findById(int id) {
        int index = getIndex(id);
        Employee employee = employeeLinkedLists[index].findByID(id);
        if (employee != null) {
            System.out.printf("在链表%d中,雇员id = %d  name=%s \n", index + 1, employee.id,employee.name);
        } else {
            System.out.println("在哈希表中,没有找到该雇员~~");
        }
    }

    /**
     * 根据雇员的Id从哈希表中删除雇员
     *
     * @param id 要删除员工的ID
     */
    public void deleteById(int id) {
        int index = getIndex(id);
        employeeLinkedLists[index].deleteById(id);
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值