二叉树常被用于实现二叉查找树和二叉堆。
树型结构常被用于大量数据的运行操作,处理效率大大高于线性结构的数据结构,
所以在数据结构中占据着极其重要的地位
二叉树
根节点:树结构的起始点
叶子节点:当树结构左右节点孩子都为空时,称为叶子节点
二叉树每个节点最多有两个孩子
二叉树每个节点最多有一个父亲
二叉树同链表一样,属于动态数据结构
静态链表和动态链表
1、静态链表是用类似于数组方法实现的,是顺序的存储结构,在物理地址上是连续的,而且需要预先分配地址空间大小。所以静态链表的长度是固定的,在做插入和删除操作时不需要移动元素,仅需修改指针。
2、动态链表是用内存申请函数动态申请内存的,所以在链表的长度上没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,要通过节点创建next指针指向下一个节点。
二分搜索树(排序树)
二分搜索树是二叉树(存储的元素具有可比较性,如:数字)
二分搜索树的每个节点的值
[图片上传中...(image.png-ddd5c4-1564728631358-0)]
·大于它左子树所有节点的值
·小于它右子树所有节点的值
·每一颗子树也是二分搜索树(树型结构的天然递归特征)
二分搜索树实现
二分搜索树创建逻辑
public class BST>{
//节点内部类
private class Node{
public E e;
public Node left,right;//左右孩子指针指向
public Node(E e){
this.e = e;
left = null;
right = null;
}
}
private Node root;//根节点
private int size;//树中存储元素数量
public BST(){
root = null;
size = 0;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
}
添加新元素
这里不设计重复元素包含
若想实现包含需定义
左子树<=节点,右子树>=节点
public void add(E e){
/*if(root == null){
root = new Node(e);
size ++;
}
else{
add(root,e);
}*/
root = add(root ,e)
}
//向以node为根的二分搜索树插入元素E,递归调用
//返回插入新节点后二分搜索树的根
private Node add(Node node,E e){
/**if(e.equals(node.e)){
return;
}
else if(e.compareTo(node.e) < 0 && node.left == null){
node.left = new Node(e);
size++;
return;
}
else if(e.compareTo(node.e) > 0 && node.right == null){
node.right = new Node(e);
size++;
return;
}*/
//把null也当成树的一个节点
if(node==null){
size ++;
return new Node(e)
}
//这里采用递归调用
if(e.compareTo(node.e) < 0){
node.left = add(node.left,e);
}
else if(e.compareTo(node.e) > 0){
node.right = add(node.right,e);
}
return node;
}
查询元素
//看二分搜索树中是否包含元素e
public boolean contains(E e){
return contains(root,e)
}
private boolean contains(Node node,E e){
if(node.e == null){
return false;
}
if(e.compareTo(node.e) == 0){
return true;
}
else if(e.compareTo(node.e) < 0){
contain(node.left,e);
}
else if(e.compareTo(node.e) > 0){
contain(node.right,e);
}
return false;
}
二分搜索树的遍历
遍历就是把所有节点都访问一遍
前序遍历:根结点 ---> 左子树 ---> 右子树
中序遍历:左子树 ---> 根结点 ---> 右子树
后序遍历:左子树 ---> 右子树 ---> 根结点
前序遍历(也可称深度优先遍历)
public void preOrder(){
preOrder(root)
}
//以node为根进行前序遍历
private void preOrder(Node node){
if(node == null){
return ;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
中序遍历(二分搜索树中序遍历结果是顺序的)
public void inOrder(){
inOrder(root);
}
private void inOrder(Node node){
if(node == null){
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
后序遍历
public void tailOrder(){
tailOrder(root);
}
private void tailOrder(Node node){
if(node == null){
return;
}
tailOrder(node.left);
tailOrder(node.right);
System.out.println(node.e);
}
应用:为二分搜索树释放资源
层序遍历(广度优先遍历)
利用队列实现
public void levelOrder(){
Queue q = new LinkedList<>();
q.add(root);
while(!q.isEmpty()){
Node cur = q.remove();
System.out.println(cur.e);
if(cur.left != null)
q.add(cur.left)
if(cur.right != null)
q.add(cur.right)
}
}
广度优先遍历的搜索效率更高效常用于算法设计中的最短路径
删除二分搜索树最大以及最小值
//查询最小值
public E minimum(){
return minimum(root).e;
}
//以node为根进行前序遍历
private E minimum(Node node){
if(node == null){
return node;
}
minimum(node.left);
}
//查询最大值
public E max(){
return max(root).e;
}
//以node为根进行前序遍历
private E max(Node node){
if(node == null){
return node;
}
minimum(node.right);
}
//删除最小值
public E removeMin(){
E ret = minimum();
root = removeMin(root);
return ret;
}
private Node removeMin(Node node){
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
//删除最大值
public E removeMax(){
E ret = max();
root = removeMax(root);
return ret;
}
private Node removeMax(Node node){
if(node.right == null){
Node leftNode = node.left;
node.left= null;
size--;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
删除二分搜索树任意元素
public void remove(E e){
root = remove(root,e);
}
private Node remove(Node node,E e){
if(node == null){
return null;
}
if(e.compareTo(node.e) < 0){
node.left = remove(node.left,e);
}
else if(e.compareTo(node.e) > 0){
node.right = remove(node.right ,e);
}
else{ // e.compareTo(node.e) == 0
//待删除节点左子树为空
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
//待删除节点右子树为空
if(node.right == null){
Node leftNode = node.left ;
node.left = null;
size --;
return leftNode;
}
//待删除节点左右子树都不为空
//找到比待删除节点大的最小节点,即右子树的最小值
//然后提取节点替代删除节点
Node successor = minimum(node.right);
//是删除了successor节点后的树
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
删除节点可以选择自己的前驱(左子树的最大值)后继(右子树的最小值)
线段树
常用于动态变化更新的数据段:
1、区间染色
2、区间查询(如:在某个注册用户群体里统计查询消费最高最低,或者消费总和)
这一类的问题常常可以用数组来实现,但不能以更好的效率确定数量体的变化,
所以时间复杂度为O(n)
而使用线段树则可以将时间复杂度达到O(logn)以达到更好的实现效率
线段树可以通过按区间范围查询,达到高效查找执行的目的
线段树是平衡二叉树
什么是平衡二叉树?
一颗树最大深度和最小深度之间的差最多为1
平衡二叉树也是一颗完全二叉树
若把线段树多余的节点看为满二叉树,
则根据树的结构,
若二叉树有h层,则应该有2h-1个节点,大约为2h
而h-1层则有2h-1个节点,
最后一层的节点数大约等于前面所有层的节点之和
基于数组的线段树的实现
public class SegmentTree {
private E[] tree;//把线段树看成满二叉树
private E[] data;
private Merger merger;
public SegmentTree(E[] arr, Merger merger){
//传入用户自己定义的融合器
this.merger = merger;
data = (E[])new Object[arr.length];
for(int i = 0; i < arr.length; i ++){
data[i] = arr[i];
}
//线段树设为满二叉树,最坏情况需要4n的节点空间构建成一棵树
tree = (E[])new Object[4 * arr.length];
buildSegmentTree(0,0,data.length - 1);
}
//三个参数分别是根节点treeIndex,根节点所对应的左区间l,以及右区间r
//如节点为sum值,则sum(l,r)
private void buildSegmentTree(int treeIndex, int l, int r){
//已经遍历到最后节点,区间两端点值相等
if(l==r){
tree[treeIndex] = data[l];
return;
}
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
//左边界+左右边界距离除2
//可以用(l + r) / 2,但为了防止数据溢出采用以下方法
int mid = l + (r - l) / 2;
buildSegmentTree(leftTreeIndex,l,mid);
buildSegmentTree(rightTreeIndex,mid+1,r);
//若线段树业务为sum求和,则
//这里使用融合接口,但未定义用户实现,抽象地进行了计算(重点)
tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
}
public int getSize(){
return data.length;
}
public E get(int index){
return data[index];
}
//树充当成数组时左孩子添加的索引
private int leftChild(int index){
return 2*index + 1;
}
//树充当成数组时右孩子添加的索引
private int rightChild(int index){
return 2*index + 2;
}
}
再定义一个融合接口merge
用于对区间进行操作,类似Comparator
public interface Merger {
E merge(E a ,E b);
}
这样就实现了一颗线段树的结构
线段树的查询
//线段树的区间查询
public E query(int queryL, int queryR){
if (queryL < 0 || queryL > data.length ||
queryR < 0 || queryR > data.length || queryL >queryR){
throw new IllegalArgumentException("Index is illegal" );
}
return query(0,0,data.length - 1,queryL,queryR);
}
private E query(int treeIndex, int l, int r, int queryL, int queryR){
//若返回的左右区间点与用户所需要的一致则返回该区间
if(l == queryL && r = queryR){
return tree[treeIndex];
}
int mid = l + (r - l) / 2;
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
//若mid+1值小于用户查询左区间,说明l值开始的子树查询不到所需的值,则直接从右子树查询
if(queryL >= mid + 1){
return query(rightTreeIndex, mid + 1 , r,queryL,queryR);
}
//若mid值大用户查询右区间,说明l值开始的子树查询不到所需的值,则直接从右子树查询
else if(queryR <= mid){
return query(leftTreeIndex,l,mid,queryL,queryR);
}
//这部分是对于值不完全包含在区间内的递归
E leftResult = query(leftTreeIndex,l,mid,queryL,mid);
E rightResult = query(rightTreeIndex,mid+1,r,mid+1,queryR);
return merger.merge(leftResult,rightResult);
}
线段树的更新
//线段树的元素更新
public void set(int index, E e){
data[index] = e;
set(0,0,data.length - 1, index, e);
}
private void set(int treeIndex, int l, int r, int index, E e){
//找到需要更新的索引
if(l == r){
tree[treeIndex] = e;
return;
}
int mid = l + (r - l) / 2;
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
c
if(index >= mid + 1){
set(rightTreeIndex,mid+1,r,index,e);
}
else if(index <= mid){
set(leftTreeIndex,l,mid,index,e);
}
tree[treeIndex] = merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
}
线段树利用其索引性质来判断查询以及更新等操作
Trie字典树(前缀树)
Trie不是一颗二叉树,是一颗多叉树
这棵树的每一个节点都有若干个指向下个节点的指针
适用于快速查询,但也有占用空间问题的劣势
构建Tire字典树
字典树指向节点利用映射TreeMap来构建
来表示每个字母对应的下一个节点
import java.util.TreeMap;
public class Trie {
private class Node{
public boolean isWord;//当访问到当前节点是是否已经拼接成一个单词
public TreeMap next;
public Node(boolean isWord){
this.isWord = isWord;
next = new TreeMap<>();
}
public Node(){
this(false);
}
}
private Node root;
private int size;
public Trie(){
root = new Node();
size = 0;
}
public int getSize(){
return size;
}
//向Trie添加一个新的单词
public void add(String word){
Node cur = root;
for(int i = 0; i < word.length(); i++){
char c = word.charAt(i);
if(cur.next.get(c) == null)
cur.next.put(c, new Node());
cur = cur.next.get(c);
}
if(!cur.isWord){
cur.isWord = true;
size ++;
}
}
//查询单词word是否存在
public boolean contains(String word){
Node cur = root;
for (int i = 0;i < word.length(); i++){
char c = word.charAt(i);
if(cur.next.get(c) == null){
return false;
}
cur = cur.next.get(c);
}
return cur.isWord;
}
}
Trie的前缀查询
//前缀查询,prefix为前缀
public boolean isPrefix(String prefix){
Node cur = root;
for (int i = 0;i < prefix.length(); i++){
char c = prefix.charAt(i);
if(cur.next.get(c) == null){
return false;
}
cur = cur.next.get(c);
}
return true;
}
Trie实现简单的模式匹配
条件:当搜索一个单词时,.
可以当作是匹配任意字母
public boolean search(String word){
return match(root, word, 0);
}
//index代表当前索引的字母
private boolean match(Node node, String word, int index){
if(index == word.length()){
return node.isWord;
}
char c = word.charAt(index);
//当匹配到的字符不是.的时候
if(c != ' . '){
if(node.next.get(c) == null){
return false;
}
return match(node.next.get(c), word, index + 1);
}
else{
//这里使用TreeMap的方法取出下一个节点所有的键
for(char nextChar: node.next.keySet){
if(match(node.next.get(nextChar), word, index + 1)){
return true;
}
return false;
}
}
}
Trie与字符串映射的应用
实现一个 MapSum
类里的两个方法,insert
和 sum
。
对于方法 insert
,你将得到一对(字符串,整数)的键值对。字符串表示键,整数表示值。如果键已经存在,那么原来的键值对将被替代成新的键值对。
对于方法 sum
,你将得到一个表示前缀的字符串,你需要返回所有以该前缀开头的键的值的总和。
import java.util.TreeMap;
class MapSum {
private class Node{
public int value;
public TreeMap next;
public Node(int value){
this.value = value;
next = new TreeMap<>();
}
public Node(){
this(0);
}
}
private Node root;
public MapSum() {
root = new Node();
}
public void insert(String word, int val) {
Node cur = root;
for(int i = 0; i < word.length(); i++){
char c = word.charAt(i);
if(cur.next.get(c) == null)
cur.next.put(c, new Node());
cur = cur.next.get(c);
}
cur.value = val;
}
public int sum(String prefix) {
Node cur = root;
for(int i = 0; i < prefix.length(); i++){
char c = prefix.charAt(i);
if(cur.next.get(c) == null){
return 0;
}
cur = cur.next.get(c);
}
return sum(cur);
}
private int sum(Node node){
//已经递归到了叶子节点
if(node.next.size() == 0){
return node.value;
}
int res = node.value;
//判断是否存在孩子节点,若有继续循环
for (char c:node.next.keySet()){
res += sum(node.next.get(c));
}
return res;
}
}