思路
- 总体框架
- 左部分排好序、右部分排好序,利用
merge
过程让 整体 有序。 merge
的核心是:谁小拷贝谁,直到左右两部分数字耗尽,然后把辅助区间拷回原数组。
- 左部分排好序、右部分排好序,利用
- 两种实现
- 递归版:自顶向下分治。
- 非递归版:自底向上,步长
step
从 1 开始每次乘 2,把相邻的两个有序块进行合并。
- 复杂度
- 时间复杂度:
T(n)=2T(n/2)+O(n) = O(n log n)
- 额外空间:
O(n)
(需要辅助数组help
来暂存合并结果) - 为什么比
O(n^2)
快? 因为比较/移动是有组织的分治合并,几乎没有“无效比较/移动”。
- 时间复杂度:
- 关于“原地归并排序”
- 有些资料说能做到
O(1)
额外空间,但通常会把时间复杂度拖到O(n^2)
,不推荐在工程/竞赛中使用。
- 有些资料说能做到
- 归并分治的便利性
- 归并的结构天然适合很多“统计/计数/对数”类问题(如逆序对、区间和、带限制的跨段统计等),能在
merge
时顺手做统计。
- 归并的结构天然适合很多“统计/计数/对数”类问题(如逆序对、区间和、带限制的跨段统计等),能在
递归版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
1) 左部分排好序、右部分排好序,利用 merge 过程让整体有序
2) merge:谁小拷贝谁,最后把辅助区间拷回原数组
3) 递归实现 & 非递归实现(自底向上 step *= 2)
4) 时间复杂度 O(n log n)
5) 额外空间 O(n)(需要 help 数组);“原地归并”虽省空间但常导致 O(n^2),不推荐
6) 归并分治是很多题的利器(逆序对、区间统计等)
*/
public static int MAXN = 5000001;
public static int[] arr = new int[MAXN];
public static int[] help = new int[MAXN];
public static int n;
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF){
n = (int) in.nval;
for(int i = 0;i<n;i++){
in.nextToken();
arr[i] = (int) in.nval;
}
mergeSort1(0,n-1); //递归方法
mergeSort2(); //非递归方法
out.print(arr[0]);
for(int i = 1; i <n ; i++){
out.print(" " + arr[i]);
}
}
}
//T(n) = 2T(n/2)+O(n)时间复杂度 O(nlogn)
//空间复杂度 O(n)
public static void mergeSort1(int l, int r){
if(l==r){
return;
}
int m = (l+r) /2;
mergeSort1(l,m);
mergeSort1(m+1,r);
merge(l,m,r);
}
//O(n)时间复杂度
public static void merge(int l , int m, int r){
int i = l;
int a =l;
int b = m+1;
while(a<=m && b<=r){
help[i++] = arr[a] <= arr[b] ? arr[a++]:arr[b++];
}
//左侧指针、右侧指针必有一个越界另一个不越界
while(a<=m){
help[i++] = arr[a++];
}
while(b<=r){
help[i++] = arr[b++];
}
for(i=l;i<=r;i++){
arr[i] = help[i];
}
}
非递归版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
///非递归:步长从1开始每次乘2,每次拿步长多的数字进行merge
public static void mergeSort2(){
//时间复杂度O(nlogn)
//空间复杂度O(n)
for(int l,m,r,step = 1;step<n;step<<=1){
l = 0;
while (l<n){
m = l+step-1;
if(m+1>=n){
//已经没有右侧
break;
}
r = Math.min(l+(step<<1)-1,n-1); //l+2x-1和n-1选最小值
//l...m m+1...r
// l...m m+1...r
// l...m m+1...r
merge(l,m,r);
l = r+1;
}
}
}
中序遍历
中序:左->根->右
- 子树头左边界进站(处理完)
- 栈弹出节点打印,节点右数重复步骤1)
- 没子树且栈空
1
2
3
4
5
6
7
8
9
// 中序(递归)
public static void inOrderRec(TreeNode head){
if (head == null) return;
inOrderRec(head.left);
System.out.print(head.val + " ");
inOrderRec(head.right);
}
后序遍历
后序:左->右->根
- 因为先序是-中左右
- 所以先序‘是-中右左
- 故后序是-左右中
1
2
3
4
5
6
7
// 后序(递归)
public static void postOrderRec(TreeNode head){
if (head == null) return;
postOrderRec(head.left);
postOrderRec(head.right);
System.out.print(head.val + " ");
}
用栈实现遍历
先序遍历(栈)
借助栈模拟:
压根节点;
弹出一个节点就打印;
先压右子再压左子(保证左子先处理)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 先序(迭代,栈)
public static void preOrderIter(TreeNode head){
if (head == null) { System.out.println(); return; }
Stack<TreeNode> stack = new Stack<>();
stack.push(head);
while (!stack.isEmpty()){
TreeNode cur = stack.pop();
System.out.print(cur.val + " ");
if (cur.right != null) stack.push(cur.right);
if (cur.left != null) stack.push(cur.left);
}
System.out.println();
}
中序遍历(栈)
一路左压到底:
- 当前指针不停向左下走并入栈;
- 左尽后,弹出栈顶打印,转向其右子树;
- 重复 1)和 2),直到“栈空且指针为空”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 中序(迭代,栈)
public static void inOrderIter(TreeNode head){
Stack<TreeNode> stack = new Stack<>();
while (head != null || !stack.isEmpty()){
if (head != null){
stack.push(head);
head = head.left;
} else {
head = stack.pop();
System.out.print(head.val + " ");
head = head.right;
}
}
System.out.println();
}
后序遍历(两个栈)
利用“先序是 中左右,若改成 中右左 再逆序,就得到 左右中(后序)”。 实现:
stack 做“改造先序”的入栈弹栈;
collect 收集弹出的顺序;
最后 collect 逐个弹出即为后序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 后序(迭代,两个栈)
public static void postOrderTwoStacks(TreeNode head){
if (head == null) { System.out.println(); return; }
Stack<TreeNode> stack = new Stack<>();
Stack<TreeNode> collect = new Stack<>();
stack.push(head);
while (!stack.isEmpty()){
TreeNode cur = stack.pop();
collect.push(cur);
if (cur.left != null) stack.push(cur.left);
if (cur.right != null) stack.push(cur.right);
}
while (!collect.isEmpty()){
System.out.print(collect.pop().val + " ");
}
System.out.println();
}
后序遍历(一个栈)
思路(关键是“最后访问节点”): 维护 lastVisited:记录上一次被打印的节点。循环不变式:
观察栈顶 cur:
若能继续向左(左子存在且未处理过),先压左;
否则若能向右(右子存在且未处理过),压右;
否则(左右都没有或都处理过),打印自己并弹出,lastVisited = cur。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 后序(迭代,一个栈)
public static void postOrderOneStack(TreeNode head){
if (head == null) { System.out.println(); return; }
Stack<TreeNode> stack = new Stack<>();
TreeNode lastVisited = null; // 上一次打印/访问完成的节点
stack.push(head);
while (!stack.isEmpty()){
TreeNode cur = stack.peek();
boolean goLeft = (cur.left != null && lastVisited != cur.left && lastVisited != cur.right);
boolean goRight = (cur.right != null && lastVisited != cur.right);
if (goLeft){
stack.push(cur.left);
} else if (goRight){
stack.push(cur.right);
} else {
System.out.print(cur.val + " ");
lastVisited = stack.pop();
}
}
System.out.println();
}