二叉树:每个节点最多有两个子树的树结构。
二叉树的性质:
满二叉树:深度为k且有2^k-1个节点的二叉树为满二叉树。
完全二叉树:深度为k,有n个节点的二叉树,当且仅当其每个节点都与深度为k的满二叉树中编号从1到n的节点一一对应,称之为完全二叉树。(除最后一层外,每一层上的节点均达到最大值,最后一层只缺少右边的若干节点)。
二叉树的实现(Java):
public TreeNode{
public TreeNode left;
public TreeNode right;
public int val;
public TreeNode(val){
this.val = val;
this.left = null;
this.right = null;
}
}
前/中/后序遍历使用递归,也就是栈的思想对二叉树进行遍历,广度优先一般使用队列的思想对二叉树进行遍历。
如果已知中序遍历和前序遍历或者中序遍历和后序遍历,那么就可以完全恢复出原二叉树结构。
其中最为关键的是前序遍历中第一个一定是根,而后序遍历最后一个一定是根,中序遍历在得知根节点后又可进一步递归得知左右子树的根节点。
但是这种方法也是有适用范围的:元素不能重复!否则无法完成定位。
对于树的三种深度优先遍历的实现方法均有递归和迭代两种实现,递归比较简单,而迭代需要用到栈。
//递归版
public List<Integer> preorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
helper(res,root);
return res;
}
private void helper(List<Integer> res, TreeNode root){
if(root == null) return;
res.add(root.val);
helper(res,root.left);
helper(res,root.right);
}
//迭代版
public List<Integer> preorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
if(root == null)return res;
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
res.add(node.val);
if(node.right!=null)stack.push(node.right);
if(node.left!=null) stack.push(node.left);
}
return res;
}
//递归版
public List<Integer> inorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
helper(res,root);
return res;
}
private void helper(List<Integer> res, TreeNode root){
if(root == null) return;
helper(res,root.left);
res.add(root.val);
helper(res,root.right);
}
//迭代版
public List<Integer> inorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
if(root == null)return res;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode node = root;
while(node!=null || !stack.isEmpty()){
if(node! = null){
stack.push(node);
node = node.left;
}else{
node = stack.pop();
res.add(node.val);
node = node.right;
}
}
return res;
}
//递归版
public List<Integer> postorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
helper(res,root);
return res;
}
private void helper(List<Integer> res, TreeNode root){
if(root == null) return;
helper(res,root.left);
helper(res,root.right);
res.add(root.val);
}
//迭代版
public List<Integer> postorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
if(root == null)return res;
stack.push(stack);
TreeNode prev = null;
while(!stack.isEmpty()){
TreeNode curr = stack.peek();
if(prev == null || prev.left == curr || prev.right == curr){
if(curr.left!=null){
stack.push(curr.left);
}else if(curr.right!=null){
stack.push(curr.right);
}
}else if(curr.left == prev){
if(curr.right!=null){
stack.push(curr.right);
}
}else{
stack.pop();
res.add(curr.val);
}
prev = curr;
}
return res;
}
public List<ArrayList<Integer>> levelOrder(TreeNode root) {
List<ArrayList<Integer>> res = new List<ArrayList<Integer>>();
if(root == null) return res;
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
ArrayList<Integer> list = new ArrayList<Integer>();
while(size>0){
TreeNode node = queue.poll();
list.add(node.val);
if(node.left!=null) queue.offer(node.left);
if(node.right!=null) queue.offer(node.right);
size--;
}
res.add(list);
}
return res;
}
最大堆:如果一棵完全二叉树的任意一个非终端节点的元素都不小于其左儿子节点和右儿子节点,则此完全二叉树为最大堆。最大堆的根节点是整个堆中最大的。
最小堆:如果一棵完全二叉树的任意一个非终端节点的元素都不大于其左儿子节点和右儿子节点,则此完全二叉树为最小堆。最小堆的根节点是整个堆中最小的。
堆的性质:
2*i+1
2*i+2
floor((i-1)/2)
堆的几个基本操作:
插入算法分为两个步骤:
上浮操作主要是将元素插入到正确的位置,假设要插入的元素为e:
e,则不用做任何调整;但若e>p
,则将e与p的位置交换。
e>p
则交换二者,直至e.
时间复杂度:O(logn),n为堆中节点总数量。由于上浮过程最多交换的次数不超过堆的高度logn,因此时间复杂度为O(logn)。
//插入元素
public void add(int item){
heap.add(item);
siftUp(heap.size()-1);
}
//对第i个元素进行上浮
public void siftUp(int index){
int e = heap.get(index);
while(index>0){
int pindex = (index-1)/2;
int parent = heap.get(pindex);
if(e>parent){
//交换二者
heap.set(index, parent);
index = pindex;
}else break;
}
heap.set(index,e);
}
一般堆中的删除指的是删除堆顶元素。
分两个步骤:
下沉操作与上浮相反,是要将当前位置的元素下沉至正确的位置。
时间复杂度:O(logn)。n为堆中节点总数量。由于下沉过程最多交换的次数不超过堆的高度logn,因此时间复杂度为O(logn)。
//删除元素
public int deleteTop(){
if(heap.size() == 0) return -1;
int max = heap.get(0);
int last = heap.remove(heap.size()-1);
if(heap.size() == 0) return last;
heap.set(0,last);
siftDown(0);
return max;
}
//下沉
public void siftDown(index){
int e = heap.get(index);
int l_index = index*2+1;
while(l_index //记录最大的孩子与最大孩子的下标
int maxChild = heap.get(l_index );
int maxIndex = l_index;
int r_index = l_index+1;
if(r_index int right = heap.get(r_index);
if(right>maxChild ){
maxChild = right;
maxIndex = r_index;
}
}
if(maxChild>e){
heap.set(index,maxChild);
index = maxIndex;
l_index = index*2+1;
}else break;
}
heap.set(index,e);
}
可以从空堆开始反复调用add接口来将所有元素插入,这种方法的时间复杂度为O(nlogn)
O(log1)+O(log2)+O(log3)+O(log4)+……+O(logn) = O(logn!) = O(nlogn)
//建堆
public void heapSort(int[] arr){
//从第一个非叶子结点开始建堆
int last = arr.length-1;
int index = (last-1)/2;
for(int i=index;i>=0;i--){
maxifyHeap(arr,i,arr.length-1);
}
//每次将堆顶元素与堆尾元素交换,并重新建堆
for(int i=arr.length-1;i>0;i--){
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
maxifyHeap(arr,0,i-1);
}
}
//调整堆,下沉过程
public void maxifyHeap(int[] arr,int i, int size){
int l_index = i*2+1;
while(l_indexint maxChild = arr[l_index];
int max_index = l_index;
int r_index = l_index+1;
if(r_indexint right = arr[r_index];
if(right>maxChild){
maxChild = right;
max_index = r_index;
}
}
if(maxChild>arr[i]){
arr[max_index] = arr[i];
arr[i] = maxChild;
i = max_index;
l_index = i*2+1;
}else break;
}
}
时间复杂度:O(nlogn)
建堆的时间为O(nlogn)。
每一次对栈顶元素的下沉调整为O(logn),共调整n次,因此时间为O(nlogn)。
给定n个权值做为n的叶子节点,构造一个二叉树,使得带权路径长度最小。这样的二叉树为最优二叉树,也为哈夫曼树。
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
二叉查找树(Binary Search Tree),亦称二叉搜索树,又称二叉排序树。
BST性质:
二分查找的时间复杂度是O(log(n)),最坏情况下的时间复杂度是O(n)(相当于顺序查找)。
又称AVL树,有如下性质:
平衡二叉树是对二叉搜索树的改进,二叉搜索树的缺点就是树的结构无法预料,最坏情况下可能是一个单支二叉树,其高度和节点数相同,就相当于一个单链表,此时查找的时间复杂度最差,为O(n)。
Trie树,即字典树
它有3个基本性质:
典型应用是用于统计和排序大量的字符串。
词频统计:在内存有限的情况下,可能不好使用hash来统计词频,因此可以用trie树来减少空间的使用,因为公共前缀都是用一个节点保存的。
前缀匹配:就拿上面的图来说吧,如果我想获取所有以”a”开头的字符串,从图中可以很明显的看到是:and,as,at,如果不用trie树,你该怎么做呢?很显然朴素的做法时间复杂度为O(N2) ,那么用Trie树就不一样了,它可以做到h,h为你检索单词的长度,