混两年的结果是学的很多,用的时候却忘的一干二净,借做题的机会梳理一下知识体系,注:本文的概念都是我看了教材或网上资料后自己背着重写的,可能存在表述不当,请勿当真
初看,很简单,这不就是穷举吗,然后简单写了一下
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] result=new int[2];
for(int i=0;i<nums.length;i++){
int a=nums[i];
for(int k=i+1;k<nums.length;k++){
if(nums[k]==target-a){
result[0]=i;
result[1]=k;
}
}
}
return result;
}
}
然后运行速度低于70%的答案,一看优质答案,Hash Table,等等,让我先复习一下。
哈希表
在传统的二叉树,表等结构中,记录在结构中随机存储,关键字与位置无联系,因此查找需要一个一个进行比较,为了提高效率,我们设想通过关键字直接找到记录的位置,这就需要在创建表时通过特定的函数为每一个关键字赋予哈希地址来存储记录,这个函数称为哈希函数,当然在实际运用中很难找到一种完美的函数来为每一个关键字赋予独有的地址,对不同的关键字可能得到同一哈希地址,这称为冲突,于是我们在建表时就需要定义一个冲突解决方案,这样建成的表称为哈希表。
哈希函数的构造方法
为了减少冲突,哈希函数应保证关键字到地址的映射尽可能均匀。
1.直接定址法
直接取关键字或对关键字进行线性处理(k*key+b)得到哈希地址
然后是伪实现(没用链表),本来想用k,v都想用int的,但是中间根据自己的值通过哈希函数算得地址然后从数组中取得自己这步实在太蠢,就写了一个超简单的学生类来做。但是这又要用到泛型——,之后再单独写吧。
public class Student {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
Student(int count){
id=count;
name=id+"aaa";
}
public String toString() {
return id+" "+name;
}
public class hash1 <E>{
int size;
Student[] hash;
public static void main(String []args) {
/*随机生成500个连续学号的学生放入表中,然后从500个学号中随机抽取20个学号,根据学号去取学生信息*/
Random ra=new Random();
int count=ra.nextInt(500)+500;
System.out.println("学号起始为"+count);
hash1<Student> ha=new hash1<>(500);
for(int i=count;i<count+500;i++) {
Student st=new Student(i);
ha.put(st);
}
for(int i=0;i<20;i++) {
int idTofind=ra.nextInt(500)+count;
System.out.println("要找的学生学号为"+idTofind);
System.out.println(ha.get(idTofind).toString());
}
}
hash1(int size){
this.size=size;
hash=new Student[size];
}
public void put(Student newS) {
int index=getIndex(newS.getId());
hash[index]=newS;
}
public int getIndex(int key) {
int index=key%size;
if(index<0) {
index+=size;
}
return index;
}
public Student get(int key) {
return hash[getIndex(key)];
}
}
2.数字分析法
一种比较偏逻辑的方法,通过分析关键字的组成来确定哈希函数,比如想同一地区身份证号前几位相同的,就将其去掉对后几位进行处理。
3.平方取中法
只告诉我结果的做法,即常用关键平方后取其中间几位作为哈希地址。
public static int getIndex(int newN) {
String newNs=newN*newN+"";
int len=newNs.length();
//取中间三位
if(len>3) {
int onePlace=newNs.charAt(len/2+1)-'0';
int tenPlace=newNs.charAt(len/2)-'0';
int hunPlace=newNs.charAt(len/2-1)-'0';
return 100*hunPlace+10*tenPlace+onePlace;
}
else {
return newN*newN;
}
}
}
4.折叠法
将关键字分为位数相同的部分,最后一位看情况,取其累加和。
5.除留余数法
对关键字取余使其结果在哈希表表长之内,也是常用的中间方法,前面几种方法结果过大时使用。
注意除数的选择会严重影响到结果的均匀分布,如除数为2的倍数时结果实际上是被除数的后几位。
一般经验,除数选择质数或不包含小于20质因数的合数。
6.随机数法
关键字长度不等时使用
冲突解决方案
1.开放定址法
当发生冲突时,给冲突地址加上一个特定的数(如果超出范围则取模)为后来的记录寻找新的地址,如果还是冲突,则在原地址加上另一个数找另一个一个地址,直到为新地址为空为止,这些数的值和顺序是固定的,组成散列。常见的散列有
(1)1,2,3…
线性探测再散列。
(2)1,-1,4,-4,9,-9…
二次探测再散列
(3)伪随机数(Random(种子)?)
伪随机数再散列。
2.再哈希表
准备多个哈希函数,一个不行换一个。
3.链地址
在每一个记录中添加一个记录的声明,没当新的记录与旧的记录发生冲突时,将新纪录的对象传给旧记录的声明,查找时从该地址的第一个记录开始查找即可。
4.建立一个公共溢出区
新建一个表,将所有发生冲突的后来者丢入表中。
查找
查找与构建类似,不过不同的是你要找的记录已经在表中了,所以不像构建时那样通过求得地址是否为空来判断是否继续(为空则填入并结束),而是要依照冲突解决方案依次序在所有可能的地址一个一个取记录比较其关键字与想要拿到记录的关键字是否相同,是则结束,否则继续。
当然查找到空地址时也要停止,这说明前面某一步出错,继续查找没有意义了。
添加简单的冲突解决方案后的实现代码为
public class hash1 <E>{
int size;
Student [] hash;
public static void main(String []args) {
Random ra=new Random();
int count;
int []testCount=new int[500];//一个数组存随机生成的学号,方便取得时候获得关键字。
hash1<Student> ha=new hash1<>(500);
//在1-10000之间随机生成200个学号,创建学生对象,将其存入哈希表。
for(int i=0;i<200;i++) {
count=ra.nextInt(10000)+1;
testCount[i]=count;
Student st=new Student(count);
ha.put(st);
}
for(int i=180;i<200;i++) {
int idTofind=testCount[i];
System.out.println("要找的学生学号为"+idTofind);
System.out.println("找到的学生信息为"+ha.SearchHash(idTofind).toString());
}
}
hash1(int size){
this.size=size;
hash=new Student [size];
}
public void put(Student newS) {
int index=getHashAddress(newS.getId());
hash[index]=newS;
}
public int hashFunction(int key) {
int temp=key%size;
if(temp<0) {
temp+=size;
}
return temp;
}
public int getHashAddress(int key) {
int initHashAddress=hashFunction(key);
int tempHashAddress=initHashAddress;
int conflictNumber=0;
while(hash[tempHashAddress]!=null) {
conflictNumber++;
tempHashAddress=manageConflict(tempHashAddress, conflictNumber);
}
return tempHashAddress;
}
public int manageConflict(int tempHashAddress,int conflictNumber) {
tempHashAddress+=conflictNumber;
if(tempHashAddress>=size) {
tempHashAddress%=size;
}
return tempHashAddress;
}
public Student SearchHash(int key) {
int initHashAddress=hashFunction(key);
if(hash[initHashAddress].getId()!=key) {
//如果发生冲突,则按冲突解决方案一个一个去找
int tempHashAddress=initHashAddress;
int conflictNumber=0;
while(hash[tempHashAddress].getId()!=key) {
conflictNumber++;
tempHashAddress=manageConflict(tempHashAddress, conflictNumber);
//检测是否为空
if(hash[tempHashAddress]==null) {
System.out.println("检索为空!");
System.exit(1);
}
}
return hash[tempHashAddress];
}
else {
return hash[initHashAddress];
}
}
}
删除
哈希表的删除大致操作与插入(构建)和查找差不多,但要注意的是需要在删除后在该位置加一个标记,否则在查找与其发生冲突的其它记录时可能因为查找到该位置为空误以为查找出错。
由于不懂java泛型,实现暂时还是以Student类为基础,添加了泛型和java中hashTable的实现模拟放在1.1和1.2中做(此处用于提醒自己)。
public class Hash {
//一个考虑到插入(构造),查找,删除的哈希表简单实现。
//为了删除,需要在删除的地方加一个标记,(我们加入一个关键字特殊的默认类)改为在关键字数组的对应位置加-1
//为了从每一个类中取得需要的元素,我们所有类都有key关键字;
//2018/11/12 Cannot create a generic array of E 暂时放弃泛型实现
Student []hash;
//关键字表,0为空,-1为删除记录后的哈希地址
int []hashKey;
//一个散列用作开放定址法,为直观,值为与初始地址差距,因此每次修改为两次值之差,为处理这样做产生的初始问题,0开头。
int []mConflict;
int size;
public Hash(int size) {
this.size=size;
hash=new Student[size];
hashKey=new int[size];
for(int i=0;i<size;i++) {
hashKey[i]=0;
}
mConflict=new int[size];
for(int i=0;i<size;i++) {
mConflict[i]=i;
}
}
//插入实现,关键字不能为0或-1
public boolean insert(Student newS,int key) {
int address=-1;
//变量count用于记录冲突次数
int count=0;
//哈希表构造方法
address=createHash(key);
//判断该地址是否已被占用
while(hashKey[address]!=0&&hashKey[address]!=-1) {
//否则调用冲突解决方法求得新地址,这里采用简单的开放定址法
count++;
if(count>size/2) {
//冲突次数过多说明构造方法不行,需要重新选择构造方法
return false;
}
if(count>=mConflict.length) {
//冲突散列过短或冲突次数过多。
return false;
}
address=(address+(mConflict[count]-mConflict[count-1]))%size;
}
//System.out.println(key+" "+address);
//将关键字和记录填入哈希表
hash[address]=newS;
hashKey[address]=key;
return true;
}
//哈希函数构造方法,除留取余
public int createHash(int key) {
return key%size;
}
public Student get(int key) {
Student result=new Student();
int address=createHash(key);
for(int i=0;i<mConflict.length;i++) {
if(hashKey[(mConflict[i]+address)%size]==0) {
//查找出错
System.exit(1);
}
if(key==hashKey[(mConflict[i]+address)%size]) {
//System.out.println("取出成功");
result=hash[(mConflict[i]+address)%size];
break;
}
}
return result;
}
public boolean delete(int key) {
int hashAddress=createHash(key);
for(int i=0;i<mConflict.length;i++) {
if(hashKey[(mConflict[i]+hashAddress)%size]==0) {
//查找出错
System.exit(1);
}
if(key==hashKey[(mConflict[i]+hashAddress)%size]) {
hash[(mConflict[i]+hashAddress)%size]=null;
hashKey[(mConflict[i]+hashAddress)%size]=-1;
return true;
}
}
//查找失败,没有这个记录
return false;
}
public Student remove(int key) {
Student result=new Student();
int address=createHash(key);
for(int i=0;i<mConflict.length;i++) {
if(hashKey[(mConflict[i]+address)%size]==0) {
//查找出错
System.exit(1);
}
if(key==hashKey[(mConflict[i]+address)%size]) {
result=hash[(mConflict[i]+address)%size];
hash[(mConflict[i]+address)%size]=null;
hashKey[(mConflict[i]+address)%size]=-1;
//System.out.println("移除成功");
break;
}
}
return result;
}
}
public class test {
public static void main(String[]args) {
Random ra=new Random();
Hash testHash=new Hash(500);
int temp[]=new int[500];
for(int i=0;i<200;i++) {
temp[i]=ra.nextInt(1000)+500;
Student st=new Student(temp[i]);
testHash.insert(st, st.getId());
}
for(int i=180;i<200;i++) {
System.out.println("取出的目录是:"+i+"关键字为:"+temp[i]);
System.out.println("查找到的学生是"+testHash.get(temp[i]).toString());
}
for(int i=160;i<180;i++) {
System.out.println("取出的目录是:"+i+"关键字为:"+temp[i]);
System.out.println("被删除的学生是"+testHash.remove(temp[i]).toString());
}
}
}
官方答案思考
因为是直接在网页上写,没有开发环境提示,列出的错误点包括语法错误
方法一
是简单的暴力破解,通过两个遍历,一个取出数组元素,另一个从剩下的元素找出满足target的元素。
值得记忆的点(对我而言):
1.内部第二次遍历不要重头开始,前面的已经遍历过了,而且这样可能把相同位置的num[i]用了两次,违反题目。
2.新建一个临时数组的写法是new int[]{}不是new int{}。
方法二
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> temp=new HashMap<>();
for(int i=0;i<nums.length;i++){
temp.put(nums[i],i);
}
for(int i=0;i<nums.length;i++){
int other=target-nums[i];
if(temp.containsKey(other)&&temp.get(other)!=i){
return new int[]{i,temp.get(other)};
}
}
return null;
}
}
自己复写的,有缺陷
非常巧妙的方法,看到方法一中内部二次遍历是通过值去找序号,联想到通过关键字去找记录,从而简化为查找哈希表。想到很容易,没想到很难,只能靠记忆。
值得记忆的点(对我而言):
1.throw new IllegalArgumentException(“No two sum solution”);向方法传递了一个不合理的参数,这种写法来处理不应该有返回值的问题。
方法三
方法二还是用了两次遍历(虽然不是嵌套的),为了精简,就想一次遍历搞定(真正大佬的思维)。
值得记忆的点(对我而言):思维上的问题。