0%

异常(范式理论解决的是什么问题)

以下的学生课程关系的函数依赖为 {Sno, Cname} -> {Sname, Sdept, Mname, Grade},键码为 {Sno, Cname}。也就是说,确定学生和课程之后,就能确定其它信息。

Sno Sname Sdept Mname Cname Grade
1 学生-1 学院-1 院长-1 课程-1 90
2 学生-2 学院-2 院长-2 课程-2 80
2 学生-2 学院-2 院长-2 课程-1 100
3 学生-3 学院-2 院长-2 课程-2 95
不符合范式的关系,会产生很多异常,主要有以下四种异常:

冗余数据:例如 学生-2 出现了两次。
修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
删除异常:删除一个信息,那么也会丢失其它信息。例如删除了 课程-1 需要删除第一行和第三行,那么 学生-1 的信息就会丢失。
插入异常:例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。

范式

范式理论是为了解决以上提到四种异常。

高级别范式的依赖于低级别的范式,1NF 是最低级别的范式。

1.第一范式 (1NF)
属性不可分。
所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值.例如:联系人的中的姓名,电话,性别,其中电话不属于第一范式,要属于第一范式的话就要对电话在进一步分裂(姓名,性别,手机,家庭电话)

2.第二范式 (2NF)
首先要满足第一范式,并且每个非主属性完全函数依赖于键码。

可以通过分解来满足。

分解前

Sno Sname Sdept Mname Cname Grade
1 学生-1 学院-1 院长-1 课程-1 90
2 学生-2 学院-2 院长-2 课程-2 80
2 学生-2 学院-2 院长-2 课程-1 100
3 学生-3 学院-2 院长-2 课程-2 95
以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖:

Sno -> Sname, Sdept
Sdept -> Mname
Sno, Cname-> Grade
Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。

Sname, Sdept 和 Mname 都只依赖于部分键码,它们可以由Sno唯一决定,与Cname无关。这样是不满足第二范式的。
分解后

关系-1

Sno Sname Sdept Mname
1 学生-1 学院-1 院长-1
2 学生-2 学院-2 院长-2
3 学生-3 学院-2 院长-2
有以下函数依赖:

Sno -> Sname, Sdept
Sdept -> Mname
关系-2

Sno Cname Grade
1 课程-1 90
2 课程-2 80
2 课程-1 100
3 课程-2 95
有以下函数依赖:

Sno, Cname -> Grade

3.第三范式 (3NF)
第三范式(3NF):在第二范式的基础上,数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。简而言之,第三范式就是属性不依赖于其它非主属性。[ 消除传递依赖 ]

上面的 关系-1 中存在以下传递函数依赖:

Sno -> Sdept -> Mname
可以进行以下分解:

关系-11

Sno Sname Sdept
1 学生-1 学院-1
2 学生-2 学院-2
3 学生-3 学院-2
关系-12

Sdept Mname
学院-1 院长-1
学院-2 院长-2

4.BCNF是3NF的改进形式

候选键:
能够确定全部属性的某个属性或某组属性,称为候选键。若候选键多于一个,则选定其中一个作为主键。

主属性:
包含在任何一个候选键中的属性,叫做主属性(Prime attribute),不包含在任何候选键中的属性称为非主属性或非键属性或非关键字段。

BCNF意味着在关系模式中每一个决定因素都包含候选键,也就是说,只要属性或属性组A能够决定任何一个属性B,则A的子集中必须有候选键。BCNF范式排除了任何属性(不光是非主属性,2NF和3NF所限制的都是非主属性)对候选键的传递依赖与部分依赖。

ACID

数据库的四大特性:

Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

ACID特性之间的关系:
只有满足一致性,事务的执行结果才是正确的。
在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
事务满足持久化是为了能应对系统崩溃的情况。

数据库如何解决并发中的一致性问题?

锁。MySQL 中提供了两种封锁粒度:行级锁以及表级锁。

应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。

但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。

下面对锁的类型进行简单介绍
1.读写锁
互斥锁(Exclusive),简写为 X 锁,又称写锁。
共享锁(Shared),简写为 S 锁,又称读锁。
有以下两个规定:

一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。

2.意向锁
使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。

在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的(因为如果一行被别的事务挂了读锁,那么这个时候就不允许再有其他程序对该表进行写入了)。

意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:

一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。

下表请按这样的逻辑理解
X:对表进行写操作,有可能写表的任意行,所以和任何操作都不兼容
IX:对表中某一行进行写操作,同时告诉别人有人在写这个表
S:对表进行读操作,可能读表的任意一行,所以对X和IX都不兼容
IS:对表的某一行进行读操作,同时告诉别人有人在读这个表,只和对表进行写操作不兼容。

任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁;
这里兼容关系针对的是表级锁,而表级的 IX 锁和行级的 X 锁兼容,两个事务可以对两个数据行加 X 锁。(事务 T1 想要对数据行 R1 加 X 锁,事务 T2 想要对同一个表的数据行 R2 加 X 锁,两个事务都需要对该表加 IX 锁,但是 IX 锁是兼容的,并且 IX 锁与行级的 X 锁也是兼容的,因此两个事务都能加锁成功,对同一个表中的两个数据行做修改。)

Problem:

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

Intuition:

可以用动态规划的思想来分析这个问题。如果用函数f(i)表示以第i个数字结尾的子数组的最大和,那么我们需要求出max[f(i)],其中0 <= i < n。我们可用如下边归公式求f(i):
动态规划表达式是:

要特别注意的是上面式子中的f[i]表示的是以i为结尾的数组的和的最大值,而非前i项所组成的数组中和最大的子数组。
这个公式的意义:当以第i-1 个数字结尾的子数组中所有数字的和小于0时,如果把这个负数与第i个数累加,得到的结果比第i个数字本身还要小,所以这种情况下以第i个数字结尾的子数组就是第i个数字本身。如果以第i-1 个数字结尾的子数组中所有数字的和大于0 ,与第i 个数字累加就得到以第i个数字结尾的子数组中所有数字的和。

Solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CodingInterview_042 {
public int FindGreatestSumOfSubArray(int[] array) {
int[] dp=new int[array.length+1];
dp[0]=0;
int maxSum=Integer.MIN_VALUE;
for(int i=0;i<array.length;i++){
dp[i+1]=Math.max(array[i],array[i]+dp[i]);
if(dp[i]>=maxSum){
maxSum=dp[i];
}
}
return maxSum;
}

public static void main(String[] args) {
int[] arr={1,-2,3,10,-4,7,2,-5};
CodingInterview_042 c=new CodingInterview_042();
System.out.println(c.FindGreatestSumOfSubArray(arr));
}
}

Problem:

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 “go” 时,第一个只出现一次的字符是 “g”。当从该字符流中读出前六个字符“google” 时,第一个只出现一次的字符是 “l”。

Intuition:

使用一个HashMap来统计字符出现的次数,同时用一个ArrayList来记录输入流,每次返回第一个出现一次的字符都是在这个ArrayList(输入流)中的字符作为key去map中查找。

Solution:

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
public class CodingInterview_041_1 {
HashMap<Character, Integer> map=new HashMap();
ArrayList<Character> list=new ArrayList<Character>();
//Insert one char from stringstream
public void Insert(char ch)
{
if(map.containsKey(ch)){
map.put(ch,map.get(ch)+1);
}else{
map.put(ch,1);
}

list.add(ch);
}

//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{ char c='#';
for(char key : list){
if(map.get(key)==1){
c=key;
break;
}
}

return c;
}
}

Problem:

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

Intuition:

首先我们需要保证左右两个堆的大小相等,大数存在最小堆里,小数存在最大堆里,这样我们就可以通过堆顶来算中位数。
由于我们需要知道的是中位数,所以自然要求最小堆和最大堆的元素数量相等,那么我们的做法是一旦数目为偶数,就让小顶堆的元素数量加1(先插入大顶堆
,但是又从大顶堆中移除一个到小顶堆,所以大顶堆的元素数量不变)。一旦为奇数,就反着来。
那么这个时候就有疑问了,为什么要让元素先进最小(大)堆,再进入最大(小)堆呢?
因为我们算中位数的时候有一个前提,即所有最大堆的元素都小于等于最小堆,那么我们把一个元素插入到了任意一边的堆中都可能影响当前的平衡,这个时候
就需要移动堆顶的元素来恢复平衡
当数目为偶数的时候,将这个值插入大顶堆中,再将大顶堆中根节点(即最大值)插入到小顶堆中;
当数目为奇数的时候,将这个值插入小顶堆中,再讲小顶堆中根节点(即最小值)插入到大顶堆中;

Solution:

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
public class CodingInterview_041 {
//小顶堆
private PriorityQueue<Integer> minHeap = new PriorityQueue<>();
//大顶堆
private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(15, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});

//记录偶数个还是奇数个
int count = 0;
//每次插入小顶堆的是当前大顶堆中最大的数
//每次插入大顶堆的是当前小顶堆中最小的数
//这样保证小顶堆中的数永远大于等于大顶堆中的数
//中位数就可以方便地从两者的根结点中获取了
public void Insert(Integer num) {
//个数为偶数的话,则先插入到大顶堆,然后将大顶堆中最大的数插入小顶堆中
if(count % 2 == 0){
maxHeap.offer(num);
int max = maxHeap.poll();
minHeap.offer(max);
}else{
//个数为奇数的话,则先插入到小顶堆,然后将小顶堆中最小的数插入大顶堆中
minHeap.offer(num);
int min = minHeap.poll();
maxHeap.offer(min);
}
count++;
}

public Double GetMedian() {
//当前为偶数个,则取小顶堆和大顶堆的堆顶元素求平均
if(count % 2 == 0){
return new Double(minHeap.peek() + maxHeap.peek())/2;
}else{
//当前为奇数个,则直接从小顶堆中取元素即可
return new Double(minHeap.peek());
}
}
}

Problem:

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

Intuition:

最小的k个数的问题用最大堆来处理,每次最大堆都把较大的数赶出去,留下来自然就是最小的k个数。想象一个场景,一个桶里有100个石头,要挑出里面最小的十个石头,我们怎么挑?把大的90个石头从桶里拿出来剩下的就是最小的十个石头了,那么挑出较大的90个石头需要的工具就是最大堆。

Solution:

public class CodingInterview_040 {
    public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
        if (k > nums.length || k <= 0)
            return new ArrayList<>();
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
        for (int num : nums) {
            maxHeap.add(num);
            if (maxHeap.size() > k)
                maxHeap.poll();
        }
        return new ArrayList<>(maxHeap);
    }

    public static void main(String[] args) {
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
        maxHeap.add(3);




        maxHeap.add(1);
        maxHeap.add(2);
        System.out.println(maxHeap.poll());
        System.out.println(maxHeap.poll());
        System.out.println(maxHeap.poll());
        //3 2 1
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        minHeap.add(3);
        minHeap.add(1);
        minHeap.add(2);
        System.out.println(minHeap.poll());
        System.out.println(minHeap.poll());
        System.out.println(minHeap.poll());
        //1 2 3
    }
}

Problem:

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

Solution:

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
public class CodingInterview_039 {
/*
第一种思路是比较正常的思路,用map记录所有数字出现的次数。然后直接进行比较就可以。
*/
public int MoreThanHalfNumWithMap_Solution(int [] array) {
int num = 0;

HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
int len = array.length ;
if(len == 0) return 0;
for(int i = 0; i < len ;i++){
if(map.containsKey(array[i])){
int count = map.get(array[i]);
++count;
map.put(array[i],count);
}else{
map.put(array[i],1);
}
}
for(Map.Entry<Integer,Integer> entry: map.entrySet()){
if(entry.getValue() > (len/2)){
num = entry.getKey();
}

}
return num;
}

/*
某个数字出现的次数大于数组长度的一半,意思就是它出现的次数比其他所有数字出现的次数和还要多。因此我们可以在遍历数组的时候记录两个值:1. 数组中的数字;2. 次数。
遍历下一个数字时,若它与之前保存的数字相同,则次数加1,否则次数减1;若次数为0,则保存下一个数字,并将次数置为1。遍历结束后,所保存的数字即为所求。最后再判断它是否符合条件。
*/
public int MoreThanHalfNum_Solution(int [] array) {
int res = array[0], count = 1;
for(int i=1; i<array.length; i++){
if(array[i] == res)
count++;
else{
count--;
}
if(count == 0){
res = array[i];
count = 1;
}
}
count = 0;
for(int i=0; i<array.length; i++){
if(array[i] == res)
count++;
}
return count > array.length/2 ? res : 0;
}

}

Problem:

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。

Intuition:

对于无重复值的情况:固定第一个字符,递归取得首位后面的各种字符串组合;再把第一个字符与后面每一个字符交换,并同样递归获得首位后面的字符串组合;递归的出口,就是只剩一个字符的时候,递归的循环过程,就是从每个子串的第二个字符开始依次与第一个字符换,然后继续处理子串。对于无重复值的情况:由于全排列就是从第一个数字起,每个数分别与它后面的数字交换,我们先尝试加个这样的判断——如果一个数与后面的数字相同那么这两个数就不交换了。例如abb,第一个数与后面两个数交换得bab,bba。然后abb中第二个数和第三个数相同,就不用交换了。但是对bab,第二个数和第三个数不 同,则需要交换,得到bba。由于这里的bba和开始第一个数与第三个数交换的结果相同了,因此这个方法不行。换种思维,对abb,第一个数a与第二个数b交换得到bab,然后考虑第一个数与第三个数交换,此时由于第三个数等于第二个数, 所以第一个数就不再用与第三个数交换了。再考虑bab,它的第二个数与第三个数交换可以解决bba。此时全排列生成完毕!

Solution:

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
public class CodingInterview_038 {


public ArrayList<String> Permutation(String str){

ArrayList<String> list = new ArrayList<String>();
if(str!=null && str.length()>0){
PermutationHelper(str.toCharArray(),0,list);
//Collections.sort可以直接按字典序打印出字符串的排列,所以我们要做的实际上只是把字符串的全排列放到list里面去就行了
Collections.sort(list);
}
return list;
}

//i表示用第i位与后面的字符交换
private void PermutationHelper(char[] chars,int i,ArrayList<String> list){
if(i == chars.length-1){
list.add(String.valueOf(chars));
}else{
Set<Character> charSet = new HashSet<Character>();
//用当前字符(下标为i的字符)放在第i位对应的字符串排列总数
charSet.add(chars[i]);
PermutationHelper(chars,i+1,list);
//将第i个字符依次与后面的字符交换放在第i位对应第字符串第排列总数。
for(int j=i+1;j<chars.length;++j){
if(!charSet.contains(chars[j])){
charSet.add(chars[j]);
swap(chars,i,j);
PermutationHelper(chars,i+1,list);
swap(chars,j,i);
}
}
}
}


private void swap(char[] cs,int i,int j){
char temp = cs[i];
cs[i] = cs[j];
cs[j] = temp;
}
}

Problem:

请实现两个函数,分别用来序列化和反序列化二叉树
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。
序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),
以 ! 表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。

Solution:

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
public class CodingInterview_037 {
public static void main(String[] args) {
//这一题我们需要先明确serialze之后表示树的字符串长成什么样子,比如一个最简单的树以1为root,2,3分别为其左右节点的树经过前序遍历的字符串打印出来应该是:1,2,#,#,3,#,#,的
TreeNode t=new TreeNode(1);
t.left=new TreeNode(2);
t.right=new TreeNode(3);
CodingInterview_037 c=new CodingInterview_037();
String s=c.Serialize(t);
System.out.println(s);
}


int index=-1;
public String Serialize(TreeNode root) {
//通过前序遍历构建二叉树字符串
StringBuilder sb = new StringBuilder();
if(root == null){
sb.append("#,");
return sb.toString();
}
sb.append(root.val + ",");
sb.append(Serialize(root.left));
sb.append(Serialize(root.right));
return sb.toString();
}
public TreeNode Deserialize(String str) {
String[] numberArray=str.split(",");
TreeNode node=DeserialzieArray(numberArray);
return node;
}
//下面这种解法是错误范例,结合例子很容易就看出来右子树的下标不对,这种情况下我们应该用全局变量来处理,让递归函数自己递增下标。
/*
private TreeNode DeserialzieArray(String[] numberArray,int index){
if(numberArray[index].equals("#")){
return null;
}else{
TreeNode node=new TreeNode(Integer.parseInt(numberArray[index]));
node.left=DeserialzieArray(numberArray,index+1);
node.right=DeserialzieArray(numberArray,index+2);
return node;
}
}
*/
//像这种感觉需要返回多个值的递归问题,我们都可以采用全局变量来处理。本题中用全局变量index来追踪index的位置
private TreeNode DeserialzieArray(String[] numberArray){
index++;
if(numberArray[index].equals("#")){
return null;
}else{
TreeNode node=new TreeNode(Integer.parseInt(numberArray[index]));
node.left=DeserialzieArray(numberArray);
node.right=DeserialzieArray(numberArray);
return node;
}
}



}

Problem:

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

Intuition:

这个题目实际上只是要我们在原来的树上添加一些边。举个简单的例子,二叉搜索树叶子节点本来左右子树都是null,经过算法处理后现在左应该指向小于它的最大元素,右子树现在应该指向大于它的最小元素。

解题思路:
1.将左子树构造成双链表,并返回链表头节点。
2.定位至左子树双链表最后一个节点。
3.如果左子树链表不为空的话,将当前root追加到左子树链表。
4.将右子树构造成双链表,并返回链表头节点。
5.如果右子树链表不为空的话,将该链表追加到root节点之后。
6.根据左子树链表是否为空确定返回的节点。

Solution:

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
public class CodingInterview_036 {
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int val){
this.val=val;
}
}

public TreeNode Convert(TreeNode root) {
if(root==null)
return null;
if(root.left==null&&root.right==null)
return root;
// 1.将左子树构造成双链表,并返回链表头节点
TreeNode left = Convert(root.left);
TreeNode p = left;
// 2.定位至左子树双链表最后一个节点
while(p!=null&&p.right!=null){
p = p.right;
}
// 3.如果左子树链表不为空的话,将当前root追加到左子树链表
if(left!=null){
p.right = root;
root.left = p;
}
// 4.将右子树构造成双链表,并返回链表头节点
TreeNode right = Convert(root.right);
// 5.如果右子树链表不为空的话,将该链表追加到root节点之后
if(right!=null){
right.left = root;
root.right = right;
}
return left!=null?left:root;
}
}