【剑指offer】1-10题:C++和Java版

剑指offer 
面试题1:赋值运算符函数
题目:如下为类型CMyString 的声明,请为该类型添加赋值符函数。

class CmyString
{
public:
    CmyString(char* pData = nullptr);
    CmyString(const CMyString& str);//构造函数中传值参数为常量引用
    ~CmyString(void);
private:
    char* m_pData;
};

写出的代码关注如下几点:
1、是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(*this)。
只有返回一个引用,才可以允许连续赋值。否则,如果函数的返回值是void,则应用该赋值运算符
将不能进行连续赋值。
2、是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参
到实参会调用一次复制构造函数。把参数声明为引用可以避免这样的无谓消耗,能提高代码的
效率。同时,在赋值运算符函数内不会改变传入的实例的状态,因此应该为传入的引用参数
加上const关键字。
3、是否释放实例自身已有的内存。如果我们忘记在分配新内存之前释放自身已有的空间,
则程序将出现内存泄露。
4、判断传入的参数和当前的实例(*this)是不是同一个。如果是同一个,则不进行赋值操作,
直接返回。当*this和传入的参数是同一个实例时,一旦释放了自身的内存,传入的参数的内存
也同时被释放了,因此再也找不到需要赋值的内容了。
在赋值运算符函数中实现异常安全性,我们有两种方法。一种简单的办法是我们先用new分配
新内容,再用delete释放已有的内容。这样只在分配内容成功之后再释放原来的内容,也就是
当分配内存失败时我们能确保CMyString的实例不会被修改。还有一种更好的办法,即先创建
一个临时实例,再交换临时实例和原来的实例。
C++版

CMyString& CMyString::operator =(const CmyString &str)
{
    if(this != &str)
    {
        CMyString strTemp(str);//创建一个临时实例strTemp
        
        char* pTemp = strTemp.m_pData;//把strTemp.m_pData和
        strTemp.m_pData = m_pData;//实例自身的m_pData进行交换
        m_pData = pTemp;
    }
    
    return *this;
}

我们先创建一个临时实例strTemp,接着把strTemp.m_pData和
实例自身的m_pData进行交换。由于strTemp是一个局部变量,但程序
运行到if的外面时也就出了该变量的作用域,就会自动调用strTemp的
析构函数,把strTemp.m_pData所指向的内存释放掉。由于strTemp.m_pData
指向的内存就是实例之前m_pData的内存,这就相当于自动调用析构函数
释放实例的内存。

面试题2:实现Singleton模式
题目:设计一个类,我们只能生成该类的一个实例。
C#版
可行的解法:加同步锁前后两次判断实例是否已经存在
我们只是在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例。
而当实例已经创建之后,我们已经不需要再执行加锁操作了。

public sealed class Singleton3 {
    private Singleton3(){
    }
    private static object syncObj = new object();
    private static Singleton3 instance = null;
    public static Singleton3 Instance{
        get{
            if(instance == null){
                lock(syncObj){
                    if(instance == null){
                        instance = new Singleton3();
                    }
                }
            return instance;
        }
    }    
}

Singleton3中只有当instance为null既没有创建时,需要加锁操作。当
instance已经创建出来之后,则无须加锁。
Singleton3用加锁机制来确保在多线程环境下只创建一个实例,并且
用两个if判断来提高效率。这样的代码实现起来比较复杂,容易出错,
我们还有更加优秀的解法。
强烈推荐的解法一:利用静态构造函数
C#的语法中有一个函数能够确保只调用一次,那就是静态构造函数,我们
可以利用C#的这个特性实现单例模式。

public sealed class Singleton4{
    private Singleton4(){
    }
    private static Singleton4 instance = new Singleton4();
    public static Singleton4 Instance{
        get{
            return instance;
        }
    }
}

我们在初始化静态变量instance的时候创建一个实例。由于C#是在调用静态构造
函数时初始化静态变量,.NET运行时能够确保只调用一次静态构造函数,这样我们
就能够保证只初始化一次instance。
强烈推荐的解法二: 实现按需创建实例
最后一个实现Singleton5则很好解决了Singleton4中的实例创建时机过早的问题。

public sealed class Singleton5{
    Singleton5(){
    }
    public static Singleton5 Instance{
        get{
            return Nested.instance;
        }
    }
    class Nested{
        static Nested(){
        }
        internal static readonly Singleton5 instance = new Singleton5(); 
    }
}

在上述SIngleton5的代码中,我们在内部定义了一个私有类型Nested。当第一次
用到这个嵌套类型的时候,会调用静态构造函数创建Singleton5的实例instance。
类型Nested只在属性Singleton5.Instance中被用到,由于其私有属性,他人无法
使用Nested类型。因此,当我们第一次试图通过属性Singleton5.Instance得到
Singleton5的实例时,会自动调用Nested的静态构造函数创建实例instance。如果
我们不调用属性Singleton5.Instance,就不会触发.NET运行调用Nested,也不会
创建实例,这样就真证做到了按需创建。
解法比较:
在Singleton3中,我们通过两次判断一次加锁确保在多线程环境中能高效地工作。
Singleton4中利用C#的静态构造函数的特性,确保只创建一个实例。在Singleton5
中利用私有嵌套类型的特性,做到只在真正需要的时候才会创建实例,提高空间使用效率。
Java版
单例模式的特点
单例类只能有一个实例;
单例类必须自己创建自己的唯一实例;
单例类必须给所有其他对象提供这一实例。
单例模式的应用
在计算机系统中,线程池、缓冲、日志对象、对话框、打印机、显卡的驱动
程序对象常被设计成单例;
这些应用多少具有资源管理器的功能。每台计算机可以有若干个打印机,但
只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。
每台计算机可以有若干通信端口,系统应集中管理这些通信端口,以避免一个
通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致
状态。
单例模式的Java代码
单例模式分为懒汉式(需要才去创建对象)和饿汉式(创建类的实例时就去创
建对象)。
饿汉式
属性实例化对象
//饿汉模式:线程安全,耗费资源。

public class HugerSingletonTest {
    //该对象的引用不可修改
    private static HugerSingletonTest Instance = new HugerSingletonTest();
    
    public static HugerSingletonTest getInstance() {
        return Instance;
    }
    private HugerSingletonTest() {
        
    }
}

在静态代码块实例对象

public class Singleton {
    private static Singleton Instance;
    
    static {
        Instance = new Singleton();
    }
    public static Singleton getInstance() {
        return Instance;
    }
    private Singleton() {
        
    }
} 

分析:饿汉式单例模式只要调用了该类,就会实例化一个对象,但有时我们并只需要调用该类的
一个方法,而不需要实例化一个对象,所以饿汉式是比较消耗资源的。
懒汉式
非线程安全

public class Singleton {
    private static Singleton Instance;
    
    public static Singleton getInstance() {
        if (null == Instance) {
            Instance = new Singleton();
        }
        return Instance;
    }
    private Singleton() {
        
    }
}

分析:如果有两个线程同时调用getInstance()方法,则会创建两个实例化对象。
所以是非线程安全的。
线程安全:给方法加锁

public class Singleton {
    private static Singleton Instance;
    public synchronized static Singleton getInstance() {
        if (null == Instance) {
            Instance = new Singleton();
        }
        return Instance;
    }
    private Singleton() {
        
    }
} 

分析:如果有多个线程调用getInstance()方法,当一个线程获取该方法,而其它线程必须等待,
消耗资源。
线程安全:双重检查锁(同步代码块)

public class Singleton {
    private static Singleton Instance;
    public synchronized static Singleton getInstance() {
        if (null == Instance) {//非空对象就不需要同步了
            synchronized (Singleton.class){//空对象的线程然后进入同步代码块
                if (null == Instace){
                    Instance = new Singleton();
                }
            }
            
        }
        return Instance;
    }
    private Singleton() {
        
    }
}

分析:双重检查锁,第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的
线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码,一个线程
进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程
进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。
线程安全:静态内部类

public class Singleton {
    private static class SingletonHodler {
        private static Singleton Instance = new Singleton9);
    }
    public sychronized static Singleton getInstacne() {
        return SingletonHodler.Instance;
    }
    private Singleton() {
        
    }
} 

分析: 利用静态内部类,某个线程调用该方法时会创建一个实例化对象。
线程安全:枚举

enum SingletonTest {
    INSTANCE;
    public void whateverMethod() {
        
    }
}

分析:枚举的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,
但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制
调用私用构造器。
指令重排序
懒汉式的双重检查版本的单例模式,不一定是线程安全的,因为在JVM的编译过程中会存在
指令重排序的问题 
其实创建一个对象,往往包含三个过程。
对于singleton = new Singleton(),这不是一个原子操作,在JVM中包含的三个过程。
1>给singleton分配内存
2>调用singleton的构造函数来初始化成员变量,形成实例
3>将singleton对象指向分配的内存空间(执行完这步singleton才是非null了)
把singleton声明成volatile,改进后的懒汉式线程(双重检查锁)的代码如下:

public class Singleton {
    //volatile的作用是:保证可见性、禁止指令重排序,但不能保证原子性
    private volatile static Singleton Instance;
    public synchronized static Singleton getInstance() {
        if (null == Instance) {//非空对象就不需要同步了
            synchronized (Singleton.class){//空对象的线程然后进入同步代码块
                if (null == Instace){
                    Instance = new Singleton();
                }
            }            
        }
        return Instance;
    }
    private Singleton() {        
    }
}

单例模式在JDK8源码中的使用
当然JDK源码中使用了大量的设计模式,Runtime类
源码如下:
//饿汉式单例模式

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    private Runtime() {
    }
    //省略多行
}

面试题3:数组中重复的数字
C++版
题目一:找出数组中的重复的数字。
在一个长度为n的数组里的所有数字都在0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,
也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},
那么对应的输出是重复的数字2或者3。
简单的方法是先把输入的数组排序。从排序的数组中找出重复的数字,排序一个长度为n的数组需要O(nlogn)的时间。
利用哈希表来解决这个问题。从头到尾按顺序扫描数组的每个数字,每扫描到一个数字的时候,都可以用O(1)的时间
来判断哈希表里是否已经包含了该数字。如果哈希表里还没有这个数字,就把它加入哈希表。如果哈希表里已经存在
该数字,就找到一个重复的数字。以一个大小为O(n)的哈希表为代价,这个算法的时间复杂度为O(n)。
我们注意到数组中的数字都在0~n-1的范围内。如果这个数组中没有重复的数字,那么当数组排序之后数字i将出现在
下标为i的位置。由于数组中有重复的数字,有些位置可能存在对个数字,同时有些位置可能没有数字。
现在让我们重排这个数组。从头到尾依次扫描这个数组中的每个数字。当扫描到下标为i的数字时,首先比较这个数字
(用m表示)是不是等于i。如果是,则接着扫描下一个数字;如果不是,则再拿它和第m个数字进行比较。如果它和第m个
数字相等,就找到了一个重复的数字(该数字在下标为i和m的位置都出现了);如果它和第m个数字不相等,就把第i数字
和第m个数字交换,把m放到属于它的位置。接下来再重复这个比较、交换的过程,直到我们发现一个重复的数字。

bool duplicate(int numbers[],int length,int* duplication){
    if(numbers == nullptr || length <=0){
        return false;
    }
    for(int i = 0; i < length; ++i){
        if(numbers[i]<0 || numbers[i]>length-1)
            return false;
    }
    for(int i = 0; i < length; ++i){
        while(number[i] != i){
            if(numbers[i] == numbers[numbers[i]]){
                *duplication = numbers[i];
                return true;
            }
            
            //swap numbers[i] and numbers[numbers[i]]
            int temp = numbers[i];
            numbers[i] = numbers[temp];
            numbers[temp] = temp;
        }
    }
    return false;
}

在上述代码中,找到的重复数字通过参数duplication传给函数的调用者,而函数的返回值表示数组中
是否有重复的数字。当输入的数组中存在重复的数字时,返回true;否则返回false。
代码中尽管有一个两重循环,但每个数字最多只要交换两次就能找到属于它自己的位置,因此总的时间
复杂度是O(n)。另外,所有的操作步骤都是在输入数组上进行的,不需要额外分配内存,因此空间复杂度为O(1)。

Java版
参数声明
numbers:输入的整数数组
length:数组的长度
duplication:输出任意一个重复的数字。使用duplication[0]表示

public boolean duplicate(int numbers[], int length, int [] duplication){
}

输出
1.如果存在重复的数字,返回值为true。并使用duplication{0}返回一个重复的数字。
2.如果不存在重复的数字,返回false。
思路1
1.遍历数组,采用hashmap存放每个元素,其中元素作为key存储,value为0.
2.当前遍历元素插入hashmap时,先检查hashmap中是否已经存在同样的key。
3.若存在,记录下该值,返回true;若不存在,存入map中,继续遍历,直到数组结束,返回false。

public boolean duplicate(int numbers[],int length,int [] duplication){
    boolean flag = false;
    if(numbers == null || length == 0){
        return flag;
    }
    HashMap map = new HashMap();
    for (int num : numbers){
        if(map.containsKey(num)){
            flag = true;
            duplication[0] = num;
            break;
        }
        map.put(num,0);
    }
    return flag;
}

思路2
1.注意到输入是长度为n的数组,所有数字都在0到n-1的范围内。可以利用原数组的特点设置标志。
2.当一个数字被访问到后,在其对应的位数上面进行-length的操作。
3.当下次访问到重复数值时,只需要检查对应的位数上面的值是否小于0,就可以判断该数字是否是重复出现。
4.该方法缺点是改变了原数组。

public boolean duplicate(int numbers[],int length,int [] duplication){
    boolean flag = false;
    if (numbers == null || length == 0) {
        return flag;
    }
    for (int i = 0; i < length; i++) {
        int index = numbers[i];
        if (index < 0){
            index += length;
        }
        if (numbers[index] < 0){
            //index<0时有一个还原+length的操作,返回的就是原来重复的数字
            duplication[0] = index;
            falg = true;
            break;
        }
        numbers[index] -= length;
    }
    return flag;
}

思路3
1.利用字符串拼接每个元素
2.判断字符串中同一个元素的子串,是否出现在了两个位置
3.StringBuffer与String的不同之处在于,String每次拼接都会返回一个新的对象,而StringBuffer不会。

public boolean duplicate(int numbers[],int length,int [] duplication){
    StringBuffer sb = new StringBuffer();
    for(int i = 0; i < length; i++){
        sb.append(numbers[i] + "");
    }
    for(int j = 0; j < length; j++){
        //判断numbers[j]+""首次出现下标和最后一次出现的下标是否相等
        if(sb.indexOf(numbers[j]+"")!= sb.lastIndexOf(numbers[j]+"")){
            duplication[0] = numbers[j];
            return true;
        }
    }
    return false;
}

面试题4:二维数组中的查找
题目描述
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。
请完成一个函数,输出这样的一个二维数组和一个整数,判断数组中是否含有该整数。
C++版
例如下面的二维数组就是每行、每列都递增排序。如果在这个数组中查找数字7,则返回true;
如果查找5,由于数组不含有该数字,则返回false。
 1  2  8  9
 2  4  9  12
 4  7  10 15
 6  8  11 15
 分析这个问题,把二维数组画成矩形,然后从数组中选取一个数字,分3中情况来分析查找的过程。
 当数组中选取的数字刚好和要查找的数字相等时,就结束查找过程。如果选取的数字小于要查找的数字,
 那么根据数组排序的规则,要查找的数字应该在当前选取位置的右边或者下边。同样,如果选取的数字
 大于要查找的数字,那么要查找的数字应该在当前选取位置的上边或者左边。
 在上面的分析中,由于要查找的数字相对于当前选取的位置有可能在两个区域中出现,而且这两个区域还有重叠。
 之所以遇到这样的难题,是因为我们在二维数组的中间来和要查找的数字进行比较,就导致下一次要查找的是两个
 相互重叠的区域。如果我们从数组的一个角上选取数字来和要查找的数字进行比较,那么情况会变得简单。
 总结查找过程,发现以下规律:首先选取数组中右上角的数字。如果该数字等于要查找的数字,则查找过程结束;
 如果该数字大于要查找的数字,则剔除这个数字所在的列; 如果该数字小于要查找的数字,则剔除这个数字所在的行;
 也就是说,如果要查找的数字不在数组的右上角,则每一次都在数组的查找范围中剔除一行或者一列,这样每一步都可以
 缩小查找的范围,直到找到要查找的数字,或者查找范围为空。

 bool Find(int* matrix, int rows, int columns,int number) {
     bool found = false;
     if (matrix != nullptr && rows > 0 && columns > 0){//指针matrix非空指针且行、列数大于0
        //定义第一行
        int row = 0;
        int column = columns - 1;
        while (row < rows && columns > 0) {
            if (matrix[row * columns + column] == number) {//数组中右上角的元素等于要查找的数字
                found = true;
                break;
            }
            else if (matrix[row * columns + column] > number) {//如果该数字大于要查找的数字
                //则剔除这个数字所在的列;
                --column;
            else//如果该数字小于要查找的数字
                //则剔除这个数字所在的行;
                ++row;
        }
     }
     return found;
 }

Java版

public class Solution{
    public boolean Find(int target, int [][] array) {
        int i = 0;
        //得到行数
        int len = array.length - 1;
        while ((len >= 0) && (i < array[0].length)) {
            
            if (array[len][i] > target) {//如果当前行数组元素大于目标值
                //则行数减一
                len--;
            }
            else if (array[len][i] < target) {//如果当前行数组元素小于目标值
                //向右走
                i++;
            }
            else {//如果当前行数组元素等于目标值
                return true;//则返回true,数组中含有该整数
            }
        }//while循环结束,未找到目标值
        return false;//返回false,数组中不含有该整数
    }
}

面试题5:替换空格
请实现一个函数,将一个字符串中的空格替换成"%20"。
例如,当字符串为We Are Happy,则经过替换之后的字符串为We%20Are%20Happy。
C++版
看到这个题目,首先应该想到的是原来一个空格字符,替换之后编程'%'、'2'和'0'这3个字符,因此字符串会变长。
如果是在原来的字符串上进行替换,就有可能覆盖修改在该字符串后面的内存。如果是创建新的字符串并在新的
字符串上进行替换,那么我们可以自己分配足够多的内存。
时间复杂度为O(n*n)的解法,不足以拿到offer。
从尾到头扫描字符串,每次碰到空格字符的时候进行替换。由于是把1个字符替换成3个字符,我们必须要把空格后面
所有的字符都后移2字节,否则就有两个字符被覆盖了。
假设字符串的长度是n。对每个空格字符,需要移动后面O(n)个字符,因此对于含有O(n)个空格字符的字符串而言,总的
时间效率是O(n*n)。
我们换一种思路,把从前向后替换改成从后向前替换。
时间复杂度为O(n)的解法,搞定Offer就靠它了
先遍历一次字符串,统计出字符串中空格的总数,由此计算出替换之后的字符串的总长度。
我们从字符串的后面开始复制和替换。首先准备两个指针:P1和P2。(a)P1指向原始字符串的末尾,而P2指向替换之后
的字符串的末尾。(b)接下来我们向前移动指针P1,逐个把它指向的字符串复制到P2指向的位置,直到碰到第一个空格
为止。(c)碰到第一个空格之后,把P1向前移动1格,在P2之前插入字符串"%20"。由于"%20"的长度为3,同时也要把P2
向前移动3格。(d)接着向前复制,直到碰到第二个空格,(e)和上次一样,我们再把P1向前移动1格,并把P2向前移动3格插入
"%20"。此时P1和P2指向同一位置,表明所有空格都已经替换完毕。
从上面的分析中,可以看出,所有的字符都只复制(移动)一次,一次这个算法的时间效率是O(n),比第一个思路要快。

/*length为字符数组String的总容量*/
void ReplaceBlank(char string[], int length){
    if(string = nullstr || length <= 0)
        return;
    /*originalLength 为字符串的实际长度*/
    int originalLength = 0;
    int numberOfBlank = 0;
    int i = 0;
    while (string[i] != '\0') {//遍历字符串,直到字符串结尾
        ++originalLength;//统计字符串长度
        if(string[i] == '')
            ++numberOfBlank;//统计空格个数
        ++i;
    }
    /*newLength 为把空格替换成'%20'之后的长度*/
    int newLength = originalLength + numberOfBlank * 2;
    if (newLength > length)
        return;
    int indexOfOriginal = originalLength;//原字符串末尾下标
    int indexOfNew = newLength;//新字符串末尾下标
    //直到新字符串索引下标和原字符串下标在同一位置且原字符串索引下标不能小于0,循环结束
    while (indexOfOriginal >= 0 && indexOfNew > indexOfOriginal) {
        if (string[indexOfOriginal] == '') {
            //替换,新串索引下标自减,向前移动
            string[indexOfNew--] = '0';
            string[indexOfNew--] = '2';
            string[indexOfNew--] = '%';
        }
        else {//复制原字符串非空字符到新串,新串索引下标自减
            string[indexOfNew--] = string[indexOfOriginal];
        }
        //原字符串索引下标向前移动
        --indexOfOriginal;
    }
}

Java版
//循环遍历字符串
//使用charAt()判断是不是空格
//使用append()追加

public class Solution{
    public String replaceSpace(StringBuffer str) {
        if (str == null) {//判断字符串是否存在
            return null;
        }
        //创建替换后的新字符串
        StringBuilder newStr = new StringBuilder();
        //循环遍历字符串
        for (int i = 0 ; i < str.length(); i++) {
            //使用charAt()判断是不是空格
            if (str.charAt() == '') {
                newStr.append("%20");//使用append()将%20追加newstr中
            }
            else {
                //将原字符串str当前字符追加到新字符串newstr中
                newStr.append(str.charAt(i));
            }
        }
        //返回该对象newStr的字符串
        return newStr.toString();
    }
}

面试题6:从尾到头打印链表
题目描述
输入一个链表,从尾到头打印链表每个节点的值
C++版
通常打印是一个只读的操作。遍历链表,第一个遍历到的节点最后一个输出,
而最后一个遍历到的节点第一个输出。典型的“后进后出”,可以用栈实现这种顺序。
每经过一个节点的时候,把该节点放到一个栈中。当遍历完整个链表后,再从栈顶
开始逐个输出节点的值,此时输出的节点的顺序已经反转过来了。
STL中std::stack 类是容器适配器,它给予程序员栈的功能
链表节点定义如下:

struct ListNode {
    int         m_nValue;
    ListNode*   m_nNext;
};
void PrintListReversingly_Iteratively(ListNode* pHead){
    //构造一个储存泛型的栈,存储的是链表数据
    std::stack nodes;
    //指针pNode指向头节点pHead
    ListNode* pNode = pHead;
    while (pNode != nullptr){//直到pNode等于空指针,循环结束
        nodes.push();//节点入栈
        //pNode这个指针指向它的(Next指针)下一个元素指针
        pNode = pNode->m_nNext;
    }//完成链表所有节点入栈
    while (!nodes.empty()){
        //pNode这个指针指向栈顶元素
        pNode = nodes.top();
        //打印pNode指针指向的元素
        printf("%d\t",pNode->m_nValue);
        //元素出栈
        nodes.pop();
    }    
}

递归在本质上就是一个栈结构,用递归实现这个函数。要实现反过来输出链表,
我们每访问到一个节点的时候,先递归输出它后面的节点,再输出该节点本身,
这样链表的输出结果就反过来了。

void PrintListReversingly_Recursively(ListNode* pHead){
    if(pHead != nullptr){//链表头节点指针不为空指针
        if(pHead->m_nNext != nullptr){//头节点Next指针不为空指针
            //递归调用方法
            PrintListReversingly_Recursively(pHead->m_nNext);
        }
        //打印pHead指针指向的元素
        printf("%d\t",pHead->m_nValue);
    }
}

基于递归的代码看起来简洁,但是链表非常长的时候,就会导致函数调用的层级
很深,从而有可能导致函数调用栈溢出。显然用栈基于循环实现的代码的鲁棒性
要好一些。
Java版

import java.util.ArrayList;
//定义一个链表
class ListNode {
    //定义一个变量val值
    int val;
    //定义next,下一个节点的引用,默认为null
    ListNode next = null;
    //构造方法,在构造时就能给val赋值
    ListNode(int val){
        this.val = val;
    }
}
public class Solution {
    //使用递归的方式
    //创建arrayList存放链表节点的值
    ArrayList arrayList = new ArrayList();
    public ArrayList printListFromTailToHead (ListNode listNode){
        if (listNode != null) {//链表节点不为null
            //递归调用方法
            this.printListFromTailToHead(listNode.next);
            //将链表节点的值添加到集合中
            arrayList.add(listNode.val);
        }
        return arrayList;
    }
}

面试题7:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列
{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
C++版
定义二叉树

struct BinaryTreeNode {
    int m_nValue;
    BinaryTreeNode* m_pLeft;
    BinaryTreeNode* m_pRight;
}

在二叉树的前序遍历序列中,第一个数字总是树的根节点的值。但在中序遍历序列中,
根节点的值在序列的中间,左子树的节点的值位于根节点值得左边,而右子树的节点
的值位于根节点值得右边。因此我们需要扫描中序遍历序列,才能找到根节点的值。
由于在中序遍历序列中,有3个数字是左子树节点的值,因此左子树共有3个左子节点。同样,
在前序遍历序列中,根节点后面的3个数字就是3个左子树节点的值,在后面的所有数字
都是右子树节点的值。这样我们就在前序遍历和中序遍历两个序列中分别找到了左、右
子树对应的子序列。我们可以用同样的方法分别构建左子树、右子树。接下来的事情可以用
递归的方法去完成。

BinaryTreeNode* Construct(int* perorder, int* inorder, int length){
    if (preorder == nullptr || ineorder == nullptr || length <= 0)
        return nullptr;
    return ConstructCore(perorder, preorder + length -1, inorder, inorder + length -1);
}
    //startPreorder指向前序遍历序列开头
    //endPreorder指向前序遍历序列末尾
    //startInorder指向中序遍历序列开头
    //endInorder指向中序遍历序列末尾
BinaryTreeNode* ConstructCore (
    int* startPreorder, int* endPreorder,
    int* startInorder, int* endInorder
)
{
    //前序遍历序列的第一个数字是根节点的值
    int rootValue = startPreorder[0];
    //创建指针指向二叉树根节点 
    BinaryTreeNode* root = new BianryTreeNode();
    //root根指向节点值 初始化值rootValue
    root->m_nValue = rootValue
    //root根指向左子树、右子树 初始化nullptr
    root->m_pLeft = root->m_pRight = nullptr;
    // 只有一个元素
    if (startPreorder == endPreorder){//先序遍历只有一个元素
        //中序遍历只有一个元素
        if (startInorder == endInorder //startInorder和endInorder指向同一位置
            && *startPreorder == *startInorder)//且*startPreorder和*startInorder指向的值相等
            return root;//返回root节点
        else //否则,抛出异常 无效输入
            throw std::exception("Invaid input");
    }
    //在中序遍历序列中找到根节点的值
    //定义、初始化指针rootInorder指向中序遍历序列的开头
    int* rootInorder = startInorder;
    //rootInorder指向根节点的值或rootInorder大于endInorder,循环结束
    while (rootInorder <= endInorder && *rootInorder != rootValue)
        ++ rootInorder;//指针rootInorder向右移动
    if (rootInorder == endInorder && *rootInorder != rootValue)
        //如果rootInorder指向中序遍历序列的末尾且未找到根节点的值
        throw std::exception("Invaild input.");//抛出异常,无效输入
    //确定二叉树的左子树节点的长度
    int leftLength = rootInorder - startInorder;
    //定义指针指向先序遍历序列左子树的末尾
    int* leftPreorderEnd = startPreorder + LeftLength;
    if (leftLength > 0){//存在左子树
        //构建左子树
        root->m_pLeft = ConstructCore(startPreorder+1, leftPreorderEnd,
            startInorder, rootInorder - 1);
    }
    if (leftLength < endPreorder - startPreorder){
        //先序遍历序列长度大于左子树长度,所有存在右子树
        //构建右子树
        root->m_pRight = ConstructCore(leftPreorderEnd + 1, endPreorder,
            rootInorder + 1, endInorder);
    }
    
    return root;
}

在函数ConstructCore中,先根据前序遍历序列的第一个数字创建根节点,接下来
在中序遍历序列中找到根节点的位置,这样就能确定左、右子树节点的数量。
在前序遍历和中序遍历序列中划分了左、右子树节点的值后,我们就可以递归
地调用ConstructCore去分别构建它的左、右子树。
Java版
思路:先找出根节点,然后利用递归方法构建二叉树

static class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x){val = x;}
}
public TreeNode reConstructBinaryTree(int [] pre,int [] in){
    if (pre == null || in == null) {
        return null;
    }
    if (pre.length == 0 || in.length == 0){
        return null;
    }
    if (pre.length != in.length) {
        return null;
    }
    //前序遍历序列的第一个数字是根节点的值
    //先找出根节点
    TreeNode root = new treeNode(pre[0]);
    利用递归方法构建二叉树
    for (int i = 0; i < pre.length; i++) {
        if (pre[0] == in[i]) {
            //构建左子树
            root.left = reConstructBinaryTree(
                            Arrays.copyOfRange(pre,1,i+1),Arrays.copyOfRange(in,0,i));
            //构建右子树                
            root.right = reConstructBinaryTree(
                            Arrays.copyOfRange(pre,i+1,pre.length),Arrays.copyOfRange(in,i+1,in.length));
        }
    }
    return root;
}

面试题8:给定一颗二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?树中的节点除了有两个分别指向
左、右子节点的指针,还有一个指向父节点的指针。
如果一个节点有右子树,那么它的下一个节点就是它的右子树的左子节点。从右子节点出发一直沿着指向左子节点的指针,
我们就能找到它的下一个节点。
接下来分析一个节点没有右子树的情形。如果节点是它父节点的左子节点,那么它的下一
个节点就是它的父节点。
如果一个节点既没有右子树,并且它还是它父节点的右子节点,我们可以沿着指向父节点的指针向上遍历,直到找到
一个是它父节点的左子节点的节点。如果这样的节点存在,那么这样的父节点就是我们要找的下一个节点。
C++版

BinaryTreeNode* GetNext(BinarytreeNode* pNode){
    if(pNode == nullptr)
        return nullptr;
    
    BinaryTreeNode* pNext = nullptr;
    if(pNode->m_pRight != nullptr){
        BinaryTreeNode* pRight = pNode->m_pRight;
        while(pRight->m_pLeft != nullptr)
            pRight = pRight->m_pLeft;
        pNext = pRight;
    }
    else if (pNode->m_pParent != nullptr){
        BinaryTreeNode* pCurrent = pNode;
        BinaryTreeNode* pParent = pNode->m_pParent;
        while(pParent != nullptr && pCurrent == pParent->m-pRight){
            pCurrent == pParent;
            pParent = pParent->m_pParent;
        }
        
        pNext = pParent;
    }
    return pNext;
}

思路:我们可发现分成两大类: 
1、有右子树的,那么下个结点就是右子树最左边的点;
2、没有右子树的,也可以分成两类: 
a)是父节点左孩子,那么父节点就是下一个节点 ; 
b)是父节点的右孩子,找他的父节点的父节点的父节点…直到当前结点是其父节点的左孩子位置。
如果没有,那么他就是尾节点,没有下一个节点。 
Java版

public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode){
        if (pNode == null)
            return null;
        //如果有右子树,则找右子树的最左节点
        if (pNode.right != null) {
            pNode = pNode.right;            
            while (pNode.left != null)
                pNode = pNode.left;
            return pNode;
        }
        //如果没有右子树,则找到第一个当前pNode节点是父节点左子节点的节点
        while (pNode.next != null){
            if(pNode.next.left == pNode)
                return pNode.next;
            pNode = pNode.next;
        }
        //退到了根节点仍没有找到,则返回null
        return null;
    }
    class TreeLinkNode {
        int val;
        TreeLinkNode left = null;
        TreeLinkNode right = null;
        TreeLinkNode next = null;
        
        TreeLinkNode(int val){
            this.val = val;
        }
    }
}

面试题9:用两个栈实现队列
题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTail和deleteHead,
分别完成在队列尾部插入节点和在队列头部删除节点的功能。

template class CQueue
{
public:
    CQueue(void);
    ~CQueue(void);
    
    void appendTail(const T& node);
    T deleteHead();

private:
    stack stack1;//栈1
    stack stack2;//栈2
};

从队列中插入元素。首先插入一个元素a,先把它插入stack1,再压入两个元素b和c,而stack2仍然是空。
从队列中删除一个元素。按照队列先入先出的规则,由于a比b、c先插入队列中,最先被删除的元素应该是a。
总结出删除一个元素的步骤:当stack2不为空时,在stack2中的栈顶元素是最先进入队列的元素,可以弹出。
当stack2为空时,我们把stack1中的元素逐个弹出并压入stack2.由于先进入队列的元素被压到stack1的底端,
经过弹出和压入操作之后就处于stack2的顶端,又可以直接弹出。

template void CQueue::appendTail(const T& element){
    stack1.push(element);//栈1插入元素
}
template T CQueue::deleteHead(){
    if(stack2.size()<=0){//栈2为空
        while(stack1.size()>0){//栈1为空
            T& data = stack1.top();
            stack1.pop();//把栈1的元素逐个弹出
            stack2.push(data);//并压入栈2
        }
    }
    if(stack2.size() == 0)
        throw new exception("queue is empty");
    T head = stack2.top();
    stack2.pop();//把栈2的元素弹出
    return head;
}

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
思路:一个栈压入元素,而另一个栈作为缓冲,将栈1的元素出栈后压入栈2中。也可以将栈1中的最后一个元素直接出栈,
而不用压入栈2中再出栈。
Java版

public void push(int node){
    stack1.push(node);
}
public int pop() throws Exception {
    if (stack1.isEmpty() && stack2.isEmpty()){
        throw new Exception("栈为空!");
    }
    if (stack2.isEmpty()){
        while(!stack1.isEmpty()){
            stack2.push(stack1.pop());
        }
    }
    return stack2.pop();
}

相关题目:用两个队列实现一个栈
我们先往栈内压入一个元素a。把a插入两个队列的任意一个,queue1.继续往栈内压入b、c两个元素,我们把它们
都插入queue1。这个时候queue1包含3个元素a、b、c,其中a位于队列的头部,c位于队列的尾部。
现在我们考虑从栈内弹出一个元素。根据栈的后入先出的原则,最后被压入栈的c应该最先被弹出。由于c位于queue1
的尾部,而我们每次只能从队列的头部删除元素,因此我们可以先从queue1中一次删除元素a、b并插入queue2,再从
queue1中删除元素c。这样就相当于从栈中弹出元素c了,我们可以用同样的方法从栈内弹出元素b。
面试题10:斐波那契数列
题目一:求斐波那契数列的第n项。
写一个函数,输入n,求斐波那契(Fibonacci)数列的第n项。
我们可以把已经得到的数列中间项保存起来,在下次需要计算的时候我们先查找一下,如果前面已经计算过就不用再重复
计算了。从下往上计算,首先根据f(0)和f(1)算出f(2),再根据f(1)和f(2)算出f(3)...以此类推就可以算出第n项了。这种
思路的时间复杂度是O(n)。

long long Fibonacci(unsigned n){
    int result[2] = {0,1};
    if(n<2)
        return result[n];
    long long fibNMinusOne = 1;
    long long fibNMinusTwo = 0;
    long long fibN = 0;
    for(usdigned int i = 2; i<=n;++i){
        fibN = fibNMinusOne+fibNMinusTwo;//从下往上计算
        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }
    return fibN;
}

时间复杂度O(logn)但不够实用的解法
把求斐波那契数列转换成求矩阵的乘方。
题目二:青蛙跳台阶问题。
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级的台阶总共有多少种跳法。有两种跳法:一种是分
两次跳,每次跳1级;另一种就是一次跳2级。n阶台阶的不同跳法的总数f(n)=f(n-1)+f(n-2)。
Java版
思路:递归的效率低,使用循环方式。

public long fibonacci(int n){
    long result = 0;
    long preOne = 1;
    long preTwo = 0;
    if(n==0){
        return preTwo;
    }
    if(n==1){
        return preOne;
    }
    for(int i = 2; i <= n; i++){
        result = preOne + preTwo;
        preTwo = preOne;
        preOne = result;
    }
    return result;
}

本题扩展:
在青蛙跳台阶的问题中,如果把条件改成:一只青蛙一次可以跳上1级台阶,也可以跳上2级......它也可以跳出n级,此时该青蛙
跳上一个n级台阶总共有多少种跳法?数学归纳法f(n)=2^n-1*1;前n-1个台阶的跳法2^n-1,面临最后一个台阶的跳法1种。
除了最后一个台阶,每个台阶都有两种选择,跳或者不跳,但是最后一个台阶必须跳。

你可能感兴趣的:(《剑指offer》)