查找算法
本质都是使用一张抽象的表格——符号表,将键值都存储在里面,然后根据指定的键来搜索并获取对应的值。关键是如何实现高效的符号表,有以下三种数据类型可以实现高效的符号表:二叉查找树、红黑树、散列表
。
符号表实现的重点在于其中使用的数据结构
和get()、put()
方法。
下面是符号表(ST)的API:
对于rank和select方法,需要额外注意一下其含义,图解如下:
经典用例:
package test;
public class FrequencyCounter {
public static void main(String[] args) {
int minlen = Integer.parseInt(args[0]); //最小键长
ST<String, Integer> st = new ST<String, Integer>();//这边直接使用了已实现的符号表,因为这是用例
//构造符号表并统计频率
while (!StdIn.isEmpty()) {
String word = StdIn.readString();
if (word.length() < minlen) continue; //忽略较短的单词
if (!st.contains(word)) st.put(word, 1);//第一次出现该键,则让该键的值设为1
else st.put(word, st.get(word) + 1);//若键已存在,则值加1
}
// 找出出现频率最高的单词
String max = "";
st.put(max, 0);
for (String word : st.keys()) { //st.keys返回的是Iterable集合,可迭代
if (st.get(word) > st.get(max))
max = word;
}
StdOut.println(max + " " + st.get(max));
}
}
顺序查找(基于无序链表)
顺序查找的含义:在符号表中若使用的数据结构是链表,那么get()方法和put()方法的实现都是遍历链表,然后再用equals()方法比较需要被查找的键和每个结点中的键值是否一样。这种方法就被称为顺序查找
。
上图是使用了基于链表的符号表的轨迹图
基于链表的实现的顺序查找十分低效。
代码:
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public class SequentialSearchST<Key, Value> {
private Node first; // the linked list of key-value pairs
private int N; // number of key-value pairs
private class Node {
private Key key;
private Value val;
private Node next;
public Node(Key key, Value val, Node next) {
this.key = key;
this.val = val;
this.next = next;
}
}
public SequentialSearchST() {
}
public int size() {
return N;
}
public boolean isEmpty() {
return size() == 0;
}
public boolean contains(Key key) {
if (key == null) throw new IllegalArgumentException("argument to contains() is null");
return get(key) != null;
}
public Value get(Key key) {
if (key == null) throw new IllegalArgumentException("argument to get() is null");
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key))
return x.val;
}
return null;
}
public void put(Key key, Value val) {
if (key == null) throw new IllegalArgumentException("first argument to put() is null");
if (val == null) {
delete(key);
return;
}
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {
x.val = val;
return;
}
}
first = new Node(key, val, first);
N++;
}
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete() is null");
first= delete(first, key);
//这边看似delete后的结点再赋给first指针有些多余,主要有以下原因:
// 万一我要删除的就是头节点,此时first显然不能指向原来的头结点。
}
// warning: function call stack too large if table is large
private Node delete(Node x, Key key) {
if (x == null) return null;
if (key.equals(x.key)) {
N--;
return x.next;//如果是first.key=key,那么就变成了first=first.next,完美删除头结点
}
x.next = delete(x.next, key);
return x;
}
public Iterable<Key> keys() {
Queue<Key> queue = new Queue<Key>();
for (Node x = first; x != null; x = x.next)
queue.enqueue(x.key);
return queue;
}
public static void main(String[] args) {
SequentialSearchST<String, Integer> st = new SequentialSearchST<String, Integer>();
for (int i = 0; !StdIn.isEmpty(); i++) {
String key = StdIn.readString();
st.put(key, i);
}
// st.put("S",null); 测试delete()方法中first= delete(first, key)中first存在与不存在的影响
for (String s : st.keys())
StdOut.println(s + " " + st.get(s));
}
}
注:
1.由于把添加的元素都放到了顶端,因此类似于栈,后进的在上面(前面),所以,上述用例的结果是
L 11
P 10
M 9
X 7
H 5
C 4
R 3
A 8
E 12
S 0
2.delete()方法中first= delete(first, key)
中 first
存在的意义到底是什么?是否可以删除?
如果我删除的是头结点,那这个first就必须存在,因为这时delete(first,key)返回的是first.next,他没有赋给first,导致,first指针没有更新,按道理得指向first.next,因此就出现了以下情况:
L 11
P 10
M 9
X 7
H 5
C 4
R 3
A 8
E 12
S 0
L依旧存在!因为first指针没有更新。
按照正确写法:first= delete(first, key),则结果如下:
P 10
M 9
X 7
H 5
C 4
R 3
A 8
E 12
S 0
Key——L不见了!!!此时first指向的是P关键字所在结点,如我们所愿。
3.那如果删除的不是头结点,fisrt存不存在结果会有变化吗,即我first指向的一直是头结点,头结点后面的结点值、链接等发生了变化后,我还能否正确遍历、增删改查等一系列操作?
试验结果表明,没变化!即只要不删头结点,first
=delete(first,key)这边的first
可有可无,因为first
默认指向的就是头结点。
二分查找(基于有序数组)
原理:使用了两个数组来分别存储Key和Value。
优点:最优的查找效率
和空间复杂度
,能够进行有序性相关操作,但插入操作
很慢。
图解:
二分查找的轨迹图:
代码:
package Chapter_3;
import java.util.NoSuchElementException;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public class BinarySearchST<Key extends Comparable<Key>, Value> {
private static final int INIT_CAPACITY = 2;
private Key[] keys;
private Value[] vals;
private int N = 0;
public BinarySearchST() {
this(INIT_CAPACITY);//调用下面的构造方法 this()方法就是用来调用其他构造函数的
}
public BinarySearchST(int capacity) {
keys = (Key[]) new Comparable[capacity];
vals = (Value[]) new Object[capacity];
}
// resize the underlying arrays
private void resize(int capacity) {
Key[] tempk = (Key[]) new Comparable[capacity];
Value[] tempv = (Value[]) new Object[capacity];
for (int i = 0; i < N; i++) {
tempk[i] = keys[i];
tempv[i] = vals[i];
}
vals = tempv;
keys = tempk;
}
public int size() {
return N;
}
public boolean isEmpty() {
return size() == 0;
}
public boolean contains(Key key) {
if (key == null) throw new IllegalArgumentException("argument to contains() is null");
return get(key) != null;
}
//get方法要用到核心方法rank();
public Value get(Key key) {
if (key == null) throw new IllegalArgumentException("argument to get() is null");
if (isEmpty()) return null; //很重要,加上,体现了你的严谨
int i = rank(key); //算出该key应该排在数组的哪里
if (i < N && keys[i].compareTo(key) == 0) return vals[i];//然后根据在数组中对应的位置,看一下是否是要找的key
return null;
}
public int rank(Key key) { //小于Key的元素的数量,注意,不取等于!!!
if (key == null) throw new IllegalArgumentException("argument to rank() is null");
int lo = 0, hi = N-1;
while (lo <= hi) { //注意,这个等号可以取到,因为mid=lo+(hi-lo)/2=lo是可以存在的
int mid = lo + (hi - lo) / 2;
int cmp = key.compareTo(keys[mid]);
if (cmp < 0) hi = mid - 1;
else if (cmp > 0) lo = mid + 1;
else return mid; //返回的是key在key数组中所对应的索引
}
return lo; //表明key在数组中不存在,返回的是key在数组中应该呆的位置
}
public void put(Key key, Value val) {
if (key == null) throw new IllegalArgumentException("first argument to put() is null");
if (val == null) {
delete(key);
return;
}
int i = rank(key);//返回的正好是key在Key[]中的索引或者应该存在的位置
//比如,rank(key)=6,代表小于key的键有6个,因此他应该是第7个元素,在数组中为Key[6].
if (i < N && keys[i].compareTo(key) == 0) { //若key在数组中存在
vals[i] = val;
return;
}
//代表key在数组中不存在,只能插入了。
if (N == keys.length) resize(2*keys.length);
for (int j = N; j > i; j--) {//keys[N]取不到的前提是N=arr.length,因为在之前有resize,因此无需担心
keys[j] = keys[j-1];//索引大于等于i的元素统统往后挪一个位置,这个等于很关键
vals[j] = vals[j-1];//因为如果i=7,就意味着key需要在数组的7索引处,但7位置有人,且比key大
}//如果N=9,i=7;则j=9,keys[9]=keys[8];j=8,keys[8]=keys[7].
keys[i] = key;
vals[i] = val;
N++;
}
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete() is null");
if (isEmpty()) return;
// compute rank
int i = rank(key); //i最小为0,即比key小的元素为0个,最大为N
// key not in table
if (i == N || keys[i].compareTo(key) != 0) {
return; //假如put("M",null),根本就没M这个键,没法删除,那就回家洗洗睡把
}
//说明数组中有key这个元素,那i后面的元素以此往前,把i位置的元素覆盖掉(即删掉)
for (int j = i; j < N-1; j++) {
keys[j] = keys[j+1];
vals[j] = vals[j+1];
}
N--;
keys[N] = null; // to avoid loitering
vals[N] = null;
// resize if 1/4 full
if (N > 0 && N == keys.length/4) resize(keys.length/2);
}
public void deleteMin() {
if (isEmpty()) throw new NoSuchElementException("Symbol table underflow error");
delete(min());
}
public void deleteMax() {
if (isEmpty()) throw new NoSuchElementException("Symbol table underflow error");
delete(max());
}
public Key min() {
if (isEmpty()) throw new NoSuchElementException("called min() with empty symbol table");
return keys[0];
}
public Key max() {
if (isEmpty()) throw new NoSuchElementException("called max() with empty symbol table");
return keys[N-1];
}
public Key select(int k) {
if (k < 0 || k >= size()) {
throw new IllegalArgumentException("called select() with invalid argument: " + k);
}
return keys[k];//就是这么一回事,简单
}
public Key floor(Key key) {//向下取整,2.4取2 小于等于key的最大键
if (key == null) throw new IllegalArgumentException("argument to floor() is null");
int i = rank(key);
if (i < N && key.compareTo(keys[i]) == 0) return keys[i];
if (i == 0) return null;
else return keys[i-1];
}
public Key ceiling(Key key) {//向上取整,2.4取3 大于等于key的最小键
if (key == null) throw new IllegalArgumentException("argument to ceiling() is null");
int i = rank(key);
if (i == N) return null;
else return keys[i];//注意这里和floor不一样,可以画一下图,一眼明了
// 0 1 2 3 假如key是F,rank(F)=3,floor(F)=keys[2],ceiling(F)=keys[3]
// A C E G 假如索引3处是F, floor(F)=keys[3],ceiling(F)=keys[3]
// 显然ceiling不太容易让人理解,原因就是不管key在不在数组中,rank(key)返回的i(3)都应该是key本应该在数组中的位置
// 即 F是夹在E 和 G的中间的,因此无论F在不在数组中,大于等于F的最小键必定都是在3的位置!
}
public int size(Key lo, Key hi) { //大于等于lo,小于等于hi的元素有多少个
if (lo == null) throw new IllegalArgumentException("first argument to size() is null");
if (hi == null) throw new IllegalArgumentException("second argument to size() is null");
if (lo.compareTo(hi) > 0) return 0;
if (contains(hi)) return rank(hi) - rank(lo) + 1;
else return rank(hi) - rank(lo);
}
public Iterable<Key> keys() {
return keys(min(), max());
}
public Iterable<Key> keys(Key lo, Key hi) {
if (lo == null) throw new IllegalArgumentException("first argument to keys() is null");
if (hi == null) throw new IllegalArgumentException("second argument to keys() is null");
Queue<Key> queue = new Queue<Key>();
if (lo.compareTo(hi) > 0) return queue;
for (int i = rank(lo); i < rank(hi); i++) //这边rank(lo)=rank(min())=0;rank(hi)=N-1
queue.enqueue(keys[i]);
if (contains(hi)) queue.enqueue(keys[rank(hi)]);//画图,但不知道为什么要分开写,为什么不合并到for循环里?
return queue;
}
public static void main(String[] args) {
BinarySearchST<String, Integer> st = new BinarySearchST<String, Integer>();
for (int i = 0; !StdIn.isEmpty(); i++) {
String key = StdIn.readString();
st.put(key, i);
}
for (String s : st.keys())
StdOut.println(s + " " + st.get(s));
}
}
二叉查找树
定义:
- 二叉查找树不是随随便便的一棵树,树中每个结点都含有一个Comparable的键以及对应的值
- 二叉查找树中每个结点的键都必须大于左子树的任意结点的键,并且小于右子树的任意结点的键
总结:
二叉查找树即在普通二叉树的基础之上加上了结点大小关系的约定或限制。
优点:
实现简单,能够进行有序性相关的操作。
缺点:
链接需要额外的空间
代码:
package Chapter_3;
import java.util.NoSuchElementException;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public class BST<Key extends Comparable<Key>, Value> {
private Node root; // 联系到链表中的first,有点内味了
private class Node {
private Key key; // sorted by key
private Value val; // associated data
private Node left, right; // left and right subtrees
private int N; // number of nodes in subtree
public Node(Key key, Value val, int N) {
this.key = key;
this.val = val;
this.N = N;
}
}
public BST() {
}
public boolean isEmpty() {
return size() == 0;
}
public int size() {
return size(root);
}
// return number of key-value pairs in BST rooted at x
private int size(Node x) {
if (x == null) return 0;
else return x.N; // x.N = 1 + size(x.left) + size(x.right);
}
public boolean contains(Key key) {
if (key == null) throw new IllegalArgumentException("argument to contains() is null");
return get(key) != null;
}
public Value get(Key key) {
//key是否为null在contains方法中其实已经判断过了,这边再判断是为了有时候直接调用get方法,因此还需要再判断一下
if (key == null) throw new IllegalArgumentException("calls get() with a null key");
return get(root, key);
}
//好好领悟递归的思想,首先root结点的键和所要找的键比较,如果key小于root结点的键,那就往左顺下一个结点,
//并再次判断大小(return get(x.left,key));如果key大于root结点的键,那就网友顺下一个结点,并判断大小
//(return get(x.right,key));如果key等于root结点的值,那就找到了,也就结束了。
//只需要用root结点当作特列来写代码即可,不要想太复杂
private Value get(Node x, Key key) {
if (x == null) return null; //树都爬遍了,最后爬到空结点了,那就宣告结果——没找到,并且结点为空,并不是什么输入异常。
int cmp = key.compareTo(x.key);
if (cmp < 0) return get(x.left, key);//get方法不需要x.left=get(x.left, key),没道理
else if (cmp > 0) return get(x.right, key);
else return x.val;
}
public void put(Key key, Value val) { //公开的方法
if (key == null) throw new IllegalArgumentException("calls put() with a null key");
if (val == null) {
delete(key);
return; //如果返回值是void,就写return,而不是return null;这边return存在是为了
//如果执行了if语句,就结束该方法,而不执行下面的赋值语句
}
root = put(root, key, val);//这边同样,如果put的不是root结点的话,其实这边的root可要可不要
}
/*这边也可以换成,上面是种不错的写法
if (val == null)
delete(key);
else
root = put(root, key, val);
*/
private Node put(Node x, Key key, Value val) { //该方法不需要公开,私有化了
if (x == null) return new Node(key, val, 1);//把他想成first结点就为null,十分好理解。
int cmp = key.compareTo(x.key);
if (cmp < 0) x.left = put(x.left, key, val);//这边返回值x.left不能省,万一要添加的就是他的左结点呢!
else if (cmp > 0) x.right = put(x.right, key, val);
else x.val = val;
x.N = 1 + size(x.left) + size(x.right); //size(Node x) return x.N
return x; //哪怕动的不是头结点,也必须return x,因为返回的x里面更新了x.N!!!
}
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("calls delete() with a null key");
root = delete(root, key);
}
private Node delete(Node x, Key key) { //该方法同样不是被直接调用的
if (x == null) return null;
int cmp = key.compareTo(x.key);
if (cmp < 0) x.left = delete(x.left, key);
else if (cmp > 0) x.right = delete(x.right, key);
else {
//找到了要删除的key,不能直接删了,还要照顾一下key的子子孙孙呀,又不是满门抄斩!
//当找到要删除的键了,第一种至多只有一个叶子节点,最简单
if (x.right == null) return x.left;
if (x.left == null) return x.right;
//第二种,两个叶子结点都完整,相对复杂
Node t = x;
x = min(t.right);//注意,这边两个方法都是调用的private类型的方法,都传Node进去了,返回的也是Node类型。
x.right = deleteMin(t.right);//deleteMin(t.right)返回的还是t.right即R这个结点
//作用是删掉了H,并且让R指向M(这些都是deleteMin干的事)
x.left = t.left;
}
x.N = size(x.left) + size(x.right) + 1; //这两句话是固定的,用来更新个数
return x;
}
public void deleteMin() {
if (isEmpty()) throw new NoSuchElementException("Symbol table underflow");
root = deleteMin(root);//更简单的实现,root=delete(min())直接搞定。万一删的是root结点呢,因此这边赋值给root不能少
}
private Node deleteMin(Node x) {
if (x.left == null) return x.right;//不是满门抄斩,因此要返回第二小的子节点
//仔细体会这边的return x.right,比如root只有右子树,那么应该删掉的是root结点
//正因为如此,才应该return root.right,然后赋给root,且如果x.left==null,那么此时x必定是最小的了,
x.left = deleteMin(x.left);//上边有root=...,这边也必须有
x.N = size(x.left) + size(x.right) + 1;
return x;
}
public void deleteMax() {
if (isEmpty()) throw new NoSuchElementException("Symbol table underflow");
root = deleteMax(root);
}
private Node deleteMax(Node x) {
if (x.right == null) return x.left;
x.right = deleteMax(x.right);
x.N = size(x.left) + size(x.right) + 1;
return x;
}
public Key min() {
if (isEmpty()) throw new NoSuchElementException("calls min() with empty symbol table");
return min(root).key;
}
private Node min(Node x) {
if (x.left == null) return x; 注意min()和deleteMin()返回的不同,一个只要查询,一个要删除最小的结点
else return min(x.left); //递归
}
public Key max() {
if (isEmpty()) throw new NoSuchElementException("calls max() with empty symbol table");
return max(root).key;
}
private Node max(Node x) {
if (x.right == null) return x;
else return max(x.right); //与min(Node x)一样
}
//寻找一个小于等于key中最大的键
public Key floor(Key key) {
if (key == null) throw new IllegalArgumentException("argument to floor() is null");
if (isEmpty()) throw new NoSuchElementException("calls floor() with empty symbol table");
Node x = floor(root, key);
if (x == null) return null; //要是没找到小于等于key的键,那就返回null
else return x.key; //找到了就返回该结点的键
}
private Node floor(Node x, Key key) {
if (x == null) return null; //找遍了都没找到,就返回Null
int cmp = key.compareTo(x.key);
if (cmp == 0) return x;
if (cmp < 0) return floor(x.left, key);//要是要找的键一直小于左子树上的结点,就一直往左下方寻找
Node t = floor(x.right, key); //发现G>E,很有可能在E的右子树中还有比G小,却一定比E大的键
if (t != null) return t; //比如说,F,G,找到了F就返回F(t),找不到就返回E(x),此时E就是一个备胎
else return x; //何时t!=null,仅当floor(x.right, key)不为空的时候,即仅当cmp==0,即找到了=key的键的时候
}
public Key ceiling(Key key) {
if (key == null) throw new IllegalArgumentException("argument to ceiling() is null");
if (isEmpty()) throw new NoSuchElementException("calls ceiling() with empty symbol table");
Node x = ceiling(root, key);
if (x == null) return null;
else return x.key;
}
private Node ceiling(Node x, Key key) {
if (x == null) return null;
int cmp = key.compareTo(x.key);
if (cmp == 0) return x;
if (cmp < 0) {
Node t = ceiling(x.left, key);
if (t != null) return t;
else return x;
}
return ceiling(x.right, key);
}
public Key select(int k) {
if (k < 0 || k >= size()) {
throw new IllegalArgumentException("argument to select() is invalid: " + k);
}
Node x = select(root, k); //x会返回null吗,返回null仅root=null时存在,此时k=size()=0,已在if语句中处理
return x.key;
}
// Return key of rank k.
private Node select(Node x, int k) {
if (x == null) return null;
int t = size(x.left); //注意这边是x.left
if (t > k) return select(x.left, k);
else if (t < k) return select(x.right, k-t-1); //这边-1是因为x也包括在里面,这样<x.right的键有t+1个
else return x;
}
public int rank(Key key) {
if (key == null) throw new IllegalArgumentException("argument to rank() is null");
return rank(key, root);
}
// Number of keys in the subtree less than key.
private int rank(Key key, Node x) {
if (x == null) return 0;
int cmp = key.compareTo(x.key);
if (cmp < 0) return rank(key, x.left); //比key大的都没用,我只要要找比key小的键的个数
else if (cmp > 0) return 1 + size(x.left) + rank(key, x.right);//这边的 1 + size(x.left)=size(x)
else return size(x.left); //我rank(H),最后返回size(H.left)=0,因为H.left=0,即最后上面的rank(key,x.right)=0
}
public Iterable<Key> keys() {
if (isEmpty()) return new Queue<Key>();
return keys(min(), max());
}
public Iterable<Key> keys(Key lo, Key hi) {
if (lo == null) throw new IllegalArgumentException("first argument to keys() is null");
if (hi == null) throw new IllegalArgumentException("second argument to keys() is null");
Queue<Key> queue = new Queue<Key>();
keys(root, queue, lo, hi);
return queue;
}
private void keys(Node x, Queue<Key> queue, Key lo, Key hi) { //这边的lo和hi就像两道屏障,要把在这个屏障里的键都添加进队列
//因此这个屏障从只有一个root大小不断扩大,直到这个屏障囊括不了x.left以及x.right为止
if (x == null) return;
int cmplo = lo.compareTo(x.key);
int cmphi = hi.compareTo(x.key);
//在这个屏障中的都可以添加进去
if (cmplo < 0) keys(x.left, queue, lo, hi);
if (cmplo <= 0 && cmphi >= 0) queue.enqueue(x.key); //返回当前方法的x,试想我给的范围是A-Z,
//可是我最小的值假如是B,遍历到B,发现B.left==null,return,此时enqueue(x.key)的x就是B结点的key
if (cmphi > 0) keys(x.right, queue, lo, hi);
}
public int size(Key lo, Key hi) {
if (lo == null) throw new IllegalArgumentException("first argument to size() is null");
if (hi == null) throw new IllegalArgumentException("second argument to size() is null");
if (lo.compareTo(hi) > 0) return 0;
if (contains(hi)) return rank(hi) - rank(lo) + 1;
else return rank(hi) - rank(lo);
}
public int height() {
return height(root);
}
private int height(Node x) {
if (x == null) return -1;
return 1 + Math.max(height(x.left), height(x.right));
}
}
图解:
平衡二叉查找树(红黑树)
优点:最优的查找和插入效率,能够进行有序性相关的操作。
缺点:链接需要额外的空间。
散列表(哈希表)
优点:能够快速地查找和插入常见类型的数据
缺点:需要计算每种类型的数据的散列;无法进行有序性相关的操作;链接和空结点需要额外的空间。
二叉树的前序、中序、后序遍历
前序遍历
访问顺序:先根节点,再左子树,最后右子树;上图的访问结果为:GDAFEMHZ。
递归实现:
public void preOrderTraverse1(Node root) {
if (root != null) { //可以换成 if(root==null) return;
System.out.print(root.val + "->");
preOrderTraverse1(root.left);
preOrderTraverse1(root.right);
}
}
非递归实现:
public void preOrderTraverse2(Node root) {
Stack<Node> stack = new Stack<>();
Node node = root;
while (node != null || !stack.empty()) {
//当遍历到最左边的结点时,此时,node.left==null,但stack不会空,应该继续遍历,两个条件缺一不可。
if (node != null) {
System.out.print(node.val + "->");//压栈之前先访问
stack.push(node);
node = node.left;//向下遍历
} else {
Node tem = stack.pop(); //向上遍历
node = tem.right;// 寻找一个结点的右子节点时,会将该节点弹出来
}
}
}
中序遍历
访问顺序:先左子树,再根节点,最后右子树;上图的访问结果为:ADEFGHMZ。
递归实现:
public void preOrderTraverse1(Node root) {
if (root != null) { //可以换成 if(root==null) return;
preOrderTraverse1(root.left);
System.out.print(root.val + "->");
preOrderTraverse1(root.right);
}
}
非递归实现:
import java.util.Stack;
public class Solution2 {
private class ListNode {
int val;
ListNode left;
ListNode right;
public ListNode(int val) {
this.val= val;
this.left = null;
this.right = null;
}
}
//中序遍历
public void theInOrderTraversal_Stack(ListNode root) {
Stack<ListNode> stack = new Stack<ListNode>();
ListNode node = root;
while (node != null || stack.size() > 0) {
if (node != null) {
stack.push(node); //直接压栈
node = node.left;
} else {
node = stack.pop(); //出栈并访问
System.out.println(node.val);
node = node.right;
}
}
}
}
牢记,前序遍历是压栈时访问,中序遍历是出栈时访问
后序遍历
访问顺序:先左子树,再右子树,最后根节点,上图的访问结果为:AEFDHZMG。
递归实现:
public void postOrderTraverse1(Node node) {
if (node== null) return;
postOrderTraverse1(node.left);
postOrderTraverse1(node.right);
System.out.print(node.val + "->");
}
非递归实现:
import java.util.Stack;
public class Solution2 {
private class ListNode {
int val;
ListNode left;
ListNode right;
public ListNode(int val) {
this.val= val;
this.left = null;
this.right = null;
}
}
public void postOrderTraverse2(ListNode node) { //AEFDHZMG
ListNode cur = null; //当前结点
ListNode pre = null; //前一个结点
Stack<ListNode> stack = new Stack<>();
stack.push(node);
while (!stack.empty()) { //根节点被弹出来后,栈为空,不满足要求,遍历结束
cur = stack.peek(); //和pop方法一个作用,不过不删除元素
//若一个结点无左右子结点了打印该节点,或遍历到左右子结点的父结点时,打印该根节点
//这个判断条件前者考虑到了左右子节点,后者考虑到了父节点(根节点)
//遍历到最后一个节点时,必定是根节点
if ((cur.left == null && cur.right == null) ||
(pre != null && (pre == cur.left || pre == cur.right))) { //这边pre!=null必不可少
System.out.print(cur.val + "->");
stack.pop(); //打印完就可以出栈了
pre = cur;
} else {
if (cur.right != null)
stack.push(cur.right); //先添加右结点,再添加左节点,顺序不能反
if (cur.left != null)
stack.push(cur.left);
}
}
}
}
层次遍历
顺序:GDMAFHZE,从上至下,从左往右
public void levelOrderTraverse(Node node) {
if (node== null) return;
Queue<Node> queue = new LinkedList<Node>();
queue.add(node);
while (!queue.isEmpty()) {
Node node2 = queue.poll();
System.out.print(node2.val + "->");
if (node2.left != null) {
queue.add(node2.left); //这边先左后右,因为这是队列,删除的时候从头开始删
}
if (node2.right != null) {
queue.add(node2.right);
}
}
}