Java哈希算法、二叉树和递归

什么是哈希算法?

散列表,又叫哈希表,它是基于快速存取的角度设计的,也是一种典型的“空间换时间”的做法。顾名思义,该数据结构可以理解为一个线性表,但是其中的元素不是紧密排列的,而是可能存在空隙。

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

比如我们存储70个元素,但我们可能为这70个元素申请了100个元素的空间。70/100=0.7,这个数字称为负载因子。我们之所以这样做,也 是为了“快速存取”的目的。我们基于一种结果尽可能随机平均分布的固定函数H为每个元素安排存储位置,这样就可以避免遍历性质的线性搜索,以达到快速存取。但是由于此随机性,也必然导致一个问题就是冲突。所谓冲突,即两个元素通过散列函数H得到的地址相同,那么这两个元素称为“同义词”。

解决冲突是一个复杂问题。冲突主要取决于:

  1. 散列函数,一个好的散列函数的值应尽可能平均分布。

  2. 处理冲突方法。

  3. 负载因子的大小。太大不一定就好,而且浪费空间严重,负载因子和散列函数是联动的。

解决冲突的办法:

  1. 线性探查法:冲突后,线性向前试探,找到最近的一个空位置。缺点是会出现堆积现象。存取时,可能不是同义词的词也位于探查序列,影响效率。

  2. 双散列函数法:在位置d冲突后,再次使用另一个散列函数产生一个与散列表桶容量m互质的数c,依次试探(d+n*c)%m,使探查序列跳跃式分布。

常用的构造散列函数的方法:

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:

  1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a?key + b,其中a和b为常数(这种散列函数叫做自身函数)

  2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

  3. 平方取中法:取关键字平方后的中间几位作为散列地址。

  4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。

  5. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。

  6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。

什么是二叉树?

在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。

一棵深度为k,且有2^k-1个结点的二叉树,称为满二叉树。这种树的特点是每一层上的结点数都是最大结点数。而在一棵二叉树中,除最后一层外,若其余层都是满的,并且或者最后一层是满的,或者是在右边缺少连续若干结点,则此二叉树为完全二叉树。具有n个结点的完全二叉树的深度为floor(log2n)+1。深度为k的完全二叉树,至少有2k-1个叶子结点,至多有2k-1个结点

动态创建二叉树:

  1. 首先创建树的左节点和右节点类
public class TreeNode {

    // 左节点(儿子)
    private TreeNode lefTreeNode;
    
    // 右节点(儿子)
    private TreeNode rightNode;
    
    // 数据
    private int value;
    
    public TreeNode(int value) {
    	this.value=value;
    }

	public TreeNode getLefTreeNode() {
		return lefTreeNode;
	}

	public void setLefTreeNode(TreeNode lefTreeNode) {
		this.lefTreeNode = lefTreeNode;
	}

	public TreeNode getRightNode() {
		return rightNode;
	}

	public void setRightNode(TreeNode rightNode) {
		this.rightNode = rightNode;
	}

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}
}
  1. 动态创建二叉树的根,插入数据时,如果节点为空,创建新节点。
public class TreeRoot {

public static void createTree(TreeRoot treeRoot, int value) {

	    //如果树根为空(第一次访问),将第一个值作为根节点
	     if (treeRoot.getTreeRoot() == null) {
	        TreeNode treeNode = new TreeNode(value);
	        treeRoot.setTreeRoot(treeNode);

	        } else  {

	            //当前树根
	            TreeNode tempRoot = treeRoot.getTreeRoot();

	    while (tempRoot != null) {
	                //当前值大于根值,往右边走
	         if (value > tempRoot.getValue()) {

	             //右边没有树根,那就直接插入
	             if (tempRoot.getRightNode() == null) {
	                 tempRoot.setRightNode(new TreeNode(value));
	                        return ;
	                   } else {
	                        //如果右边有树根,到右边的树根去
	                       tempRoot = tempRoot.getRightNode();
	                    }
	                } else {
	                 //左没有树根,那就直接插入
	               if (tempRoot.getLefTreeNode() == null) {
	                     tempRoot.setLefTreeNode(new TreeNode(value));
	                        return;
	                    } else {
	                 //如果左有树根,到左边的树根去
	                 tempRoot = tempRoot.getLefTreeNode();
	                    }
	                }
	            }
	        }
	    }
	 }
  1. 遍历
public class TreeRoot {
	 private TreeNode treeRoot;

	    public TreeNode getTreeRoot() {
	        return treeRoot;
	    }

	    public void setTreeRoot(TreeNode treeRoot) {
	        this.treeRoot = treeRoot;
	    }
	    
	    /**
	     * 先序遍历
	     * @param rootTreeNode  根节点
	     */
	    public static void preTraverseBTree(TreeNode rootTreeNode) {

	        if (rootTreeNode != null) {

	            //访问根节点
	            System.out.println(rootTreeNode.getValue());

	            //访问左节点
	            preTraverseBTree(rootTreeNode.getLefTreeNode());

	            //访问右节点
	            preTraverseBTree(rootTreeNode.getRightNode());
	        }
	    }
	    /**
	     * 中序遍历
	     * @param rootTreeNode  根节点
	     */
	    public static void inTraverseBTree(TreeNode rootTreeNode) {

	        if (rootTreeNode != null) {

	            //访问左节点
	            inTraverseBTree(rootTreeNode.getLefTreeNode());

	            //访问根节点
	            System.out.println(rootTreeNode.getValue());

	            //访问右节点
	            inTraverseBTree(rootTreeNode.getRightNode());
	        }
	    }
	 }

二叉树的遍历:

二叉树遍历分为三种:前序、中序、后序,其中序遍历最为重要。

  • 先序遍历:遍历顺序规则为【根左右】 先访问根节点,在左叶子,右叶子

  • 中序遍历:遍历顺序规则为【左根右】

  • 后序遍历:遍历顺序规则为【左右根】

在这里插入图片描述
比如上图正常的一个满节点,A:根节点、B:左节点、C:右节点,前序顺序是ABC(根节点排最先,然后同级先左后右);中序顺序是BAC(先左后根最后右);后序顺序是BCA(先左后右最后根)。

在这里插入图片描述
比如上图二叉树遍历结果:

前序遍历:ABCDEFGHK

中序遍历:BDCAEHGKF

后序遍历:DCBHKGFEA

分析中序遍历如下图,中序比较重要,java很多树排序是基于中序。

在这里插入图片描述

什么是递归?

程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。但是如果没终止条件会造成死循环,所以递归代码里要有结束自调自的条件。接下来通过一个案例来学习如何使用递归算法计算自然数之和,如例Example1.java。

public class Example1 {
         public static void main(String[] args) {
              int sum=getsum(4);              //调用递归方法,获得1~4的和
              System.out.println("sum="+sum);  //打印结果
         }
              //下面的方法使用递归实现求1~n的和
              public static int getsum(int n) {
                 if(n==1{
                  //满足条件,递归结束
                  return 1;
          }
               int temp=getSum(n-1);
                 return temp+n;
      }
}

运行结果:

sun = 10

Example1.java中,定义了一个 getSum()方法用于计算1~n之间自然数之和。例程中的12行代码相当于在 getSum()方法的内部调用了自身,这就是方法的递归,整个递归过在n==1时结束。整个递归过程中 getsum()方法被调用了4次,每次调用时,n的值都会递减。当n的值为1时,所有递归调用的方法都会以相反的顺序相继结束,所有的返回值会进行累加,最终得到结果10。

使用递归时需要注意的问题:

  1. 递归就是方法里调用自身。

  2. 在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。

  3. 递归算法代码显得很简洁,但递归算法解题的运行效率较低。所以不提倡用递归设计程序。

  4. 在递归调用的过程中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等,所以一般不提倡用递归算法设计程序。

  5. 在做递归算法的时候,一定把握出口,也就是做递归算法必须要有一个明确的递归结束条件。这一点是非常重要的。其实这个出口就是一个条件,当满足了这个条件的时候我们就不再递归了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值