查找算法&&哈希表
一、查找算法
1 查找算法的介绍
- 在java中,我们常用的查找算法有四种:
- 顺序(线性)查找
- 二分法查找
- 插值查找
- 斐波那契查找
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 插值查找算法的原理介绍:
- int[] arr = {1, 8, 10, 89, 1000, 1234};
- 将折半查找中的求mid 索引的公式 , low 表示左边索引left, high表示右边索引right;
- 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 插值查找注意事项
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
- 关键字分布不均匀的情况下,该方法不一定比折半查找要好
5 斐波那契(黄金分割法)查找算法
5.1 斐波那契(黄金分割法)查找基本介绍
- 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
- 斐波那契数列 {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的理解:
- 由斐波那契数列 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;
- 类似的,每一子段也可以用相同的方式分割
- 但顺序表长度n不一定刚好等于F[k]-1,所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能使得F[k]-1恰好大于或等于n即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1到F[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不是从低到高插入,但要求各条链表仍是从低到高,怎么解决?]
- 使用链表来实现哈希表, 该链表不带表头[即: 链表的第一个结点就存放雇员信息]
- 思路分析并画出示意图
- 代码实现[增删改查(显示所有员工,按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);
}
}