剑指Offer(第一版)

2021/5/12 18:57:41

本文主要是介绍剑指Offer(第一版),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

除了剑指Offer第一版书中提到的思路,更有剑走偏锋的刁钻题解,欢迎一起变强

面试题1:赋值运算符函数

面试题2:实现Singleton模式

面试题3:二维数组中的查找

面试题4:替换空格

面试题5:从尾到头打印链表

面试题6:重建二义树

面试题7 :用两个栈实现队列

面试题8 :旋转数组的最小数字

面试题9 :斐波那契数列

面试题10 :二进制中1的个数

面试题11 :数值的整数次方

快速幂精讲

面试题12 :打印1到最大的n位数

回溯与全排列

面试题13 :在O(1)时间删除链表结点

面试题14 :调整数组顺序使奇数位于偶数前面

面试题15 :链表中倒数第k个结点

面试题16 :反转链表

面试题17 :合并两个排序的链表

面试题18 :树的子结构

二叉树的遍历之递归和非递归的广度、深度优先遍历

面试题19 :二叉树的镜像

面试题20 :顺时针打印矩阵

热点知识:快速幂(11)、回溯(12)、全排列(12)、斐波那契数列问题转化为求矩阵的n次方(9)、二叉树的遍历(18)

面试题1:赋值运算符函数

给定一个类如下,要求重载赋值运算符

class CMyString{
public:
	CMyString(char* pData = nullptr);
	CMyString(const CMyString& str);
	~CMyString(void);
private:
	char* m_pData;
};

需要考虑如下四个点:

  1. 重载赋值操作符的成员方法的参数应该使用引用类型(可以常量引用为了安全考虑),这样可以减少拷贝构造临时对象产生的系统开销
  2. 对象自己给自己赋值,则直接返回
  3. 正确释放被赋值的对象空间,并开辟和值拷贝。千万避免浅拷贝
  4. 为了达到连续赋值的目的,返回值应该给自身的引用

普通的参考如下:

class CMyString{
public:
	CMyString(char* pData = nullptr);
	CMyString(const CMyString& str);
	~CMyString(void);
	CMyString& operator=(const CMyString& str) { //1.引用传参
		if (this == &str) //2.对象自己给自己赋值则直接返回
			return *this;
		//3.正确释放空间,开辟空间和值拷贝
		delete []this->m_pData; //针对简单类型delete 和 delete[]是一样的,对于连续的class类型必须使用delete[],不然会内存泄露
		this->m_pData = new char[strlen(str.m_pData)+1]; //之所以要str.m_pData)+1是因为字符串的结束标志'\0'
		//值拷贝
		strcpy(this->m_pData, str.m_pData);
		return *this; //4.返回自身引用为了连续赋值
	}
private:
	char* m_pData;
};

异常安全的代码应该如下:

class CMyString {
public:
	CMyString(char* pData = nullptr) {
		this->m_pData = new char[strlen(pData) + 1];
		strcpy(this->m_pData, pData);
	}
	CMyString(const CMyString& str) {
		this->m_pData = new char[strlen(str.m_pData) + 1];
		strcpy(this->m_pData, str.m_pData);
	}
	CMyString& operator=(const CMyString& str) { //1.引用传参
		if (this != &str) { //2.对象自己给自己赋值则返回
			CMyString tmp(str); //拷贝构造(需要自行实现深拷贝版本)
			char* m_pData_tmp = this->m_pData;
			this->m_pData = tmp.m_pData;
			tmp.m_pData = m_pData_tmp;
			//3.正确释放空间,开辟空间和值拷贝
		}
		return *this; //4.返回自身引用为了连续赋值
	}
	char* getCString() {
		return this->m_pData;
	}
	~CMyString();
private:
	char* m_pData;
};

运行测试:

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;
class CMyString {
public:
	CMyString(char* pData = nullptr) {
		this->m_pData = new char[strlen(pData) + 1];
		strcpy(this->m_pData, pData);
	}
	CMyString(const CMyString& str) {
		this->m_pData = new char[strlen(str.m_pData) + 1];
		strcpy(this->m_pData, str.m_pData);
	}
	CMyString& operator=(const CMyString& str) { //1.引用传参
		if (this != &str) { //2.对象自己给自己赋值则返回
			CMyString tmp(str); //拷贝构造(需要自行实现深拷贝版本)
			char* m_pData_tmp = this->m_pData;
			this->m_pData = tmp.m_pData;
			tmp.m_pData = m_pData_tmp;
			//3.正确释放空间,开辟空间和值拷贝
		}
		return *this; //4.返回自身引用为了连续赋值
	}
	char* getCString() {
		return this->m_pData;
	}
	~CMyString();
private:
	char* m_pData;
};
CMyString::~CMyString(){}
int main() {
	char* str = new char[3]();
	str[0] = 'k';
	str[1] = 'o';
	CMyString cm1(str);
	CMyString cm2 = cm1;
	printf("%p\n", cm1.getCString());
	printf("%p\n", cm2.getCString());
	return 0;
}

对象各自的空间是独立的且没有发生浅拷贝,即使重载赋值操作符成员函数中临时对象实例化失败,比如内存不足抛出bad_alloc异常,此时还未对对象原有内存释放,实例的状态是有效的,保证了异常安全性

面试题2:实现Singleton模式

第一种思路(线程不安全):构造函数私有化,设置一个静态实例和静态方法返回这个静态实例

class Singleton {
public:
	static Singleton* getInstance() { //静态成员方法用于返回单例实例
		if (instance == nullptr)
			instance = new Singleton();
		return instance;
	}
private:
	static Singleton* instance; //静态成员存储单例
	Singleton(); //构造函数私有化

};
Singleton* Singleton::instance = nullptr;

只要调用getInstance静态方法就会返回单例实例,这种方法缺点就是线程不安全,只适用于单线程环境。getInstance静态成员方法没有加锁,所现称环境下,实例有重复创建的风险。

第二种思路(线程安全,但效率低):在第一种的基础上给静态方法中的临界区加锁

std::mutex mtx;
class Singleton {
public:
	static Singleton* getInstance() { //静态成员方法用于返回单例实例
		mtx.lock();
		if (instance == nullptr)
			instance = new Singleton();
		mtx.unlock();
		return instance;
	}
private:
	static Singleton* instance; //静态成员存储单例
	Singleton(); //构造函数私有化

};
Singleton* Singleton::instance = nullptr;

mutex同步锁的头文件:#include

一个时刻只有一个线程可以得到同步锁,因此单例实例不会发生重复重建,加锁机制的目的是在多线程环境下只创建一个实例

每次通过静态成员方法getInstance获得单例实例,又都会先加锁,比较耗时间,因此考虑双判断,一旦单例实例已经创建了,就不再加锁而是直接返回

第三种思路(线程安全,双判断提高效率):对第二种思路改进,加双判断一旦单例实例已经创建就不在加锁直接返回

std::mutex mtx;
class Singleton {
public:
	static Singleton* getInstance() { //静态成员方法用于返回单例实例
		if (instance == nullptr) {
			mtx.lock();
			if (instance == nullptr)
				instance = new Singleton();
			mtx.unlock();
		}
		return instance;
	}
private:
	static Singleton* instance; //静态成员存储单例
	Singleton(); //构造函数私有化

};
Singleton* Singleton::instance = nullptr;

第四种思路(利用静态构造函数特性,限C#语言):利用静态构造函数只被调用一次的特性

public sealed class Singleton{
    private Singleton(){}
    private static Singleton instance = new Singleton();
    public static Singleton getInstance{
        get{
            return instance;
        }
    }
}

唯一的缺点是:程序员不能控制静态构造函数的调用时机。.NET运行时发现第一次使用一个类型的时候会自动调用该类型的静态构造函数。

第五种思路(私有嵌套类型的特性控制实例创建时机,限C#语言):利用内部类第一次被调用的同时创建Singleton实例

public sealed class Singleton{
    Singleton(){}
    public static Singleton getInstance{
        get{
            return Nested.instance;
        }
    }
    
    class Nested{
        static Nested(){}
        
        internal static readonly Singleton instance = new Singleton(); //internal修饰的变量在同一个程序集的文件中,内部类型或者是成员才可以访问
    }
}

还是利用第一次使用一个类型的时候才会自动调用该类型的静态构造方法的特性。给Singleton内定义一个内部私有类Nested,使用Singleton的静态成员方法getInstance第一次使用这个嵌套类型的时候,调用静态构造函数创建Singleton的实例instance

面试题3:二维数组中的查找

题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序, 每•列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一 个二维数组和一个整数,判断数组中是否含有该整数。

思路:

起始位置从右上角开始,如果当前位置元素比target小,则row++;如果当前位置元素比target大,则col–;如果相等,返回true

如果越界了还没找到,说明不存在,返回false

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        if (!matrix.size() || !matrix[0].size()) return false;
        //以主对角线为划分,遇到第一个大于target的元素,则在上一个对角线元素和当前这个对角线元素中间遍历所有元素就一定可以找到
        int row = matrix.size(); //n
        int col = matrix[0].size(); //m
        int i = 0, j = col - 1; //右上角下标
        while (i < row && j >= 0 ) {
            if (target > matrix[i][j]) i++;
            else if (target < matrix[i][j]) j--;
            else return true; //target == matrix[i][j]
        }
        return false;
    }
};

面试题4:替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"

使用辅助空间,时间复杂度O(n)

C++
class Solution {
public:
    string replaceSpace(string s) {
        int len = s.size();
        string tmp = "";
        for (int i = 0; i < len; ++i) {
            if (s[i] == ' ')
                tmp += "%20";
            else
                tmp += s[i];
        }
        return tmp;
    }
};
C++
class Solution {
public:
    string replaceSpace(string s) {
        int len = s.size();
        string tmp = "";
        for (int i = 0; i < len; ++i) {
            if (s[i] == ' '){
                tmp += "%20";
                continue;
            }
            tmp += s[i];
        }
        return tmp;
    }
};
C
char* replaceSpace(char* s) {
	int countBlank = 0, len = 0;
	char* tmp = s;
	while (*tmp != '\0') {
		if (*tmp++ == ' ')
			countBlank++;
		len++;
	}
	//使用辅助空间
	tmp = (char*)malloc(len + 1 + countBlank * 2);
	memset(tmp, 0, len + 1 + countBlank * 2);
		int index = len - 1 + countBlank * 2;
	for (int i = len - 1; i >= 0; --i) {
		if (s[i] != ' ')
			tmp[index--] = s[i];
		else {
			tmp[index--] = '0';
			tmp[index--] = '2';
			tmp[index--] = '%';
		}
	}
	return tmp;
}

不使用辅助空间C

假设在原始字符串上替换,且确保输入的字符串后面有足够的内存空间

思路1:从前往后遍历字符串,每遇到一个空格,就将空格后面的字符总共向后挪动2个字符,遇到一个空格相当于向后挪动提供出来供"%20"三个字符替换的空间,时间复杂度挺差O(n^2)

思路2:遍历一遍,数清楚多少个空格数x,从原字符最后一个字符后面的2x个空间处往后挪,每遇到一个空格就用"%20"插入。思路的精髓在于先预留够每个空格替换之后增加的2个字符空间然后挪动,整体时间复杂度O(2n)–>O(n)

C,时间复杂度O(n)(OJ中没有提供足够的内存,请在本地IDE测试)
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
char* replaceSpace(char* s) {
	int countBlank = 0, len = 0;
	char* tmp = s;
	while (*tmp != '\0') {
		if(*tmp++ == ' ')
			countBlank++;
		len++;
	}
	int index = len-1 + countBlank * 2;
	for (int i = len-1; i >= 0; --i) {
		if (s[i] != ' ')
			s[index--] = s[i];
		else {
			s[index--] = '0';
			s[index--] = '2';
			s[index--] = '%';
		}
	}
	return s;
}

int main() {
	char str[18] = "We are happy.";
	str[17] = 0;
	cout<<replaceSpace(str)<<endl;
	return 0;
}

面试题5:从尾到头打印链表

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)

思路1

很明显给定的是一个单向链表,可以将链表逆转完成,构造一个数组返回

这道题的效率取决于单向链表逆转的时间复杂度上,因此采用遍历原链表的同时以头插的方式构造新链表,遍历新链表构造数组返回就完成任务了,时间复杂度就是O(n)

直接对原链表进行逆转
C
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* reversePrint(struct ListNode* head, int* returnSize){
    struct ListNode* newList = NULL;
    int count = 0; //记录链表个数
    while(head != NULL){ //链表逆转
        struct ListNode* cur = head;
        head = head->next;
        cur->next = newList;
        newList = cur;
        count++;
    }
    *returnSize = count; //returnSize是一个输出型参数,记录链表节点个数
    int * res = (int*)malloc(count*sizeof(int)); //开辟结果数组空间
    count = 0; //count临时变量重利用
    while(newList != NULL){ //遍历列表赋值数组
        res[count++] = newList->val;
        newList = newList->next;
    }
    return res;
}

参数returnSize是一个输出型参数,记录链表节点个数

C++
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        ListNode* newList = NULL;
        int count = 0; //记录链表个数
        while(head != NULL){ //链表逆转
            ListNode* cur = head;
            head = head->next;
            cur->next = newList;
            newList = cur;
            count++;
        }
        vector<int> res(count, 0);
        count = 0; //count临时变量重利用
        while(newList != NULL){ //遍历列表赋值数组
            res[count++] = newList->val;
            newList = newList->next;
        }
        return res;
    }
}; 
不对原链表进行逆转,重新建立一个新的链表
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* reversePrint(struct ListNode* head, int* returnSize){
    struct ListNode* newList = NULL;
    int count = 0; //记录链表个数
    while(head != NULL){ //链表逆转
        struct ListNode* mal = (struct ListNode*)malloc(sizeof(struct ListNode));
        mal->val = head->val;
        head = head->next;
        mal->next = newList;
        newList = mal;
        count++;
    }
    *returnSize = count; //returnSize是一个输出型参数,记录链表节点个数
    int * res = (int*)malloc(count*sizeof(int)); //开辟结果数组空间
    count = 0; //count临时变量重利用
    while(newList != NULL){ //遍历列表赋值数组
        res[count++] = newList->val;
        newList = newList->next;
    }
    return res;
}
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        ListNode* newList = NULL;
        int count = 0; //记录链表个数
        while(head != NULL){ //链表逆转
            ListNode* mal = (ListNode*)malloc(sizeof(ListNode));
            mal->val = head->val;
            head = head->next;
            mal->next = newList;
            newList = mal;
            count++;
        }
        vector<int> res(count, 0); //开辟结果数组空间
        count = 0; //count临时变量重利用
        while(newList != NULL){ //遍历列表赋值数组
            res[count++] = newList->val;
            newList = newList->next;
        }
        return res;
    }
};

思路2

首先遍历一遍得到链表节点数(目的是开辟合适大小空间的结果数组),再遍历链表正向初始化数组,然后将数组逆转也就是链表逆转,总共三次遍历搞定

C
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* reversePrint(struct ListNode* head, int* returnSize){
    //得到节点数
    int count = 0;
    struct ListNode* tmp = head;
    while(tmp != NULL){
        count++;
        tmp = tmp->next;
    }
    tmp = head;
    *returnSize = count;
    int * res = (int*)malloc(count*sizeof(int));
    //遍历数组得到初步结果数组
    count = 0;
    while(tmp != NULL){
        res[count++] = tmp->val;
        tmp = tmp->next;
    }
    //结果数组逆转
    for(int i=0; i<count>>1; ++i){
        res[i] ^= res[count-1-i];
        res[count-1-i] ^= res[i];
        res[i] ^= res[count-1-i];
    }
    return res;
}
C++
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        //得到节点数
        int count = 0;
        struct ListNode* tmp = head;
        while(tmp != NULL){
            count++;
            tmp = tmp->next;
        }
        tmp = head;
        vector<int> res(count, 0);
        //遍历数组得到初步结果数组
        count = 0;
        while(tmp != NULL){
            res[count++] = tmp->val;
            tmp = tmp->next;
        }
        //结果数组逆转
        for(int i=0; i<count>>1; ++i){
            res[i] ^= res[count-1-i];
            res[count-1-i] ^= res[i];
            res[i] ^= res[count-1-i];
        }
        return res;
    }
};

糊涂糊涂呀,为什么要先遍历单向链表并结果数组从0下标开始(正向初始化),逆向赋值岂不是都不用将结果数组逆转了,一次遍历得到节点数量,第二次遍历得到结果数组,总共两次遍历搞定

C

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* reversePrint(struct ListNode* head, int* returnSize){
    //得到节点数
    int count = 0;
    struct ListNode* tmp = head;
    while(tmp != NULL){
        count++;
        tmp = tmp->next;
    }
    tmp = head;
    *returnSize = count;
    int * res = (int*)malloc(count*sizeof(int));
    //遍历数组得到初步结果数组
    count-=1;
    while(tmp != NULL){
        res[count--] = tmp->val;
        tmp = tmp->next;
    }
    return res;
}

C++

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        //得到节点数
        int count = 0;
        struct ListNode* tmp = head;
        while(tmp != NULL){
            count++;
            tmp = tmp->next;
        }
        tmp = head;
        vector<int> res(count, 0);
        //遍历数组得到初步结果数组
        count--;
        while(tmp != NULL){
            res[count--] = tmp->val;
            tmp = tmp->next;
        }
        return res;
    }
};

思路3

根据思路2的遍历单向链表的同时逆向初始化结果数组,目的就是为了后进先出,得到启发;可以使用栈结构来完成逆序

C语言的话,和逆向初始化结果数组是一样的逻辑,都是为了后进先出,就认为逆向初始化数组就是压栈的方式,不做代码展示

利用C++栈来逆序
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        stack<int, vector<int>> st;
        while(head != nullptr){
            st.push(head->val);
            head = head->next;
        }
        vector<int> res(st.size(), 0);
        int index = 0;
        while(!st.empty()){
            res[index++] = st.top();
            st.pop();
        }
        return res;
    }
};

思路4:递归方式实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> reversePrint(ListNode* head) {
        vector<int> res;
        revreversePrint_(head, res);
        return res;
    }
    void revreversePrint_(ListNode* head, vector<int>& res){
        if(!head) return ;
        revreversePrint_(head->next, res);
        res.push_back(head->val);
    }
};

使用本地编译器测试

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
typedef struct ListNode {
	int val;
	struct ListNode* next;
}ListNode;
void reversePrint(ListNode * head) {
	if (head == nullptr) return ;
	reversePrint(head->next);
	cout << head->val<<" ";
}
int main() {
	//构造一个1-->3-->2-->nullptr的单向链表
	ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));
	head->val = 1;
	head->next = (struct ListNode*)malloc(sizeof(struct ListNode));
	head->next->val = 3;
	head->next->next = (struct ListNode*)malloc(sizeof(struct ListNode));
	head->next->next->next = nullptr;
	head->next->next->val = 2;
	reversePrint(head);
	return 0;
}

总结:

在OJ中发现先遍历链表得到初步结果数组,然会对数组进行逆转的执行用时更短,更占优势,但是内存占用会多一点,因为数组逆转需要临时变量辅助实现

  • 思路1可以对原链表进行逆转,也可以构造一个新的链表从而实现逆序输出单链表的任务

  • 思路2没有对原单链表的结构进行任何改变,通过遍历单向链表正向初始化结果数组再对数组逆转即可得到结果,更简单的方式是遍历单向链表的同时逆向初始化结果数组

  • 思路3受到思路2的启发逆序的子问题就是后进先出,所以使用栈结构就可以轻松解决,由于增加了栈容器的空间开销,内存销毁会比直接通过结果数组逆向初始化大一点

面试题6:重建二义树

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,给出

前序遍历 preorder = [3,9,20,15,7]

中序遍历 inorder   = [9,3,15,20,7]

返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7

。。。。。。暂时不会

面试题7 :用两个栈实现队列

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

思路

  • 设置一个Pop栈和一个Push栈,Pop栈用于出队列,Push栈用于入队列

  • 入队列前,首先将Pop栈依次出栈压入到Push栈,然后Push栈入队

  • 出队列前,首先将Push栈依次出栈压入到Pop栈,然后Pop栈出队

C++
class CQueue {
public:
    CQueue() {

    }
    
    void appendTail(int value) {
        //首先将Pop栈全挪到Push栈
        while(!Pop.empty()){
            Push.push(Pop.top());
            Pop.pop();
        }
        Push.push(value);
    }
    
    int deleteHead() {
        //首先将Push栈全部挪到pop栈
        while(!Push.empty()){
            Pop.push(Push.top());
            Push.pop();
        }
        if(Pop.empty())return -1;
        int tmp = Pop.top();
        Pop.pop();
        return tmp;
    }
private:
    stack<int, vector<int>> Push;
    stack<int, vector<int>> Pop;
};

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue* obj = new CQueue();
 * obj->appendTail(value);
 * int param_2 = obj->deleteHead();
 */
C(挺麻烦的,首先客观上得先实现一个栈)
//实现栈结构
typedef struct stack{
    int* arr;
    int sz;
    int cp;
}stack;

//栈相关操作
int top(stack* st){
    return st->arr[st->sz-1];
}
bool pop(stack* st){
    st->sz--;
    return true;
}
bool push(stack* st, int val){
    if(st->sz < st->cp){ //容量够
        st->arr[st->sz++] = val;
    }else{
        //扩容
        int* tmp = (int*)malloc(sizeof(int)*st->cp*2);
        memcpy(tmp, st->arr, sizeof(int)*st->cp);
        int* del = st->arr;
        st->arr = tmp;
        st->cp *= 2;
        free(del);
    }
    return true;
}
bool empty(stack* st){
    return st->sz == 0;
}
//初始化栈操作
void initializeStack(stack* obj, int sz){
    obj->arr = (int*)malloc(sz*sizeof(int));
    obj->cp = sz;
    obj->sz = 0;
}

//队列结构
typedef struct {
    stack Push;
    stack Pop;
} CQueue;


CQueue* cQueueCreate() {
    CQueue* tmp = (CQueue*)malloc(sizeof(CQueue));
    initializeStack(&tmp->Push, 1000);
    initializeStack(&tmp->Pop, 1000);
    return tmp;
}

void cQueueAppendTail(CQueue* obj, int value) {
    //首先将Pop栈全挪到Push栈
    while(!empty(&obj->Pop)){
        push(&obj->Push, top(&obj->Pop));
        pop(&obj->Pop);
    }
    push(&obj->Push, value);
}

int cQueueDeleteHead(CQueue* obj) {
    //首先将Push栈全部挪到pop栈
    while(!empty(&obj->Push)){
        push(&obj->Pop, top(&obj->Push));
        pop(&obj->Push);
    }
    if(empty(&obj->Pop))return -1;
    int tmp = top(&obj->Pop);
    pop(&obj->Pop);
    return tmp;
}

void cQueueFree(CQueue* obj) {
    while(!empty(&obj->Push)) pop(&obj->Push);
    while(!empty(&obj->Pop)) pop(&obj->Pop);
}

/**
 * Your CQueue struct will be instantiated and called as such:
 * CQueue* obj = cQueueCreate();
 * cQueueAppendTail(obj, value);
 
 * int param_2 = cQueueDeleteHead(obj);
 
 * cQueueFree(obj);
*/

面试题8 :旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。

思路及分析

剑指offer书本上吧啦吧啦说一大堆,思想就是二分的思想,首先数组首尾元素分别记录下标,中间元素下标就是两个下标的均值,若是中间元素位于前面的递增数组(arr[mid] >= arr[left]),最小元素位于该中间元素的后面([mid+1, left])。其实举个栗子就能明白:

给一个四个元素的数组:1, 2, 3, 4

所有旋转数组如下:

2 3 4 1

3 4 1 2

4 1 2 3

将旋转之后的数组划分成成两个排序的子数组

2 3 4 1

3 4 1 2

4 1 2 3

前面的子数组的元素都大于或者等于后面子数组的元素,最小刚好是这两个子数组的分界线。因此在排序的数组中利用二分查找法实现O(logn)的时间复杂度查找算法。

思路

利用两个指针分别指向数组的第一个元素和最后一个元素。第一个元素大于等于最后一个元素

找到数组的中间元素,如果该中间元素位于前面的递增子数组,则中间元素一定大于等于第一个指针指向的元素,最小元素就位于该中间元素后面,第一个指针指向该中间元素;如果该中间元素位于后面的递增数组,则中间元素小于等于最后一个指针指向的元素,此时最小元素就位于该中间元素的前面,第二个指针指向该中间元素

手动模拟以上过程,发现第一个指针总是在前面的递增数组元素中移动;第二个指针总是在后面的递增数组元素中移动。最终第一个指针和第二个指针都会指向相邻两个元素,且第二个指针指向的就是最小元素

1
class Solution {
public:
    int minArray(vector<int>& numbers) {
        int left = 0;
        int right = numbers.size()-1;
        int mid = 0;
        while(left+1 < right){
            mid = (left+right)>>1;
            if(numbers[mid]>=numbers[left])
                left = mid;
            else if(numbers[mid]<=numbers[right])
                right = mid;
        }
        return numbers[right];
    }
};

不能通过所有的测试用例:数组整体递增有序

2

当第一个元素小于最后一个元素时,说明整个数组递增有序,直接返回第一个元素

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int left = 0;
        int right = numbers.size()-1;
        int mid = 0;
        if(numbers[left] < numbers[right]) return numbers[left];
        while(left+1 < right){
            mid = (left+right)>>1;
            if(numbers[mid]>=numbers[left])
                left = mid;
            else if(numbers[mid]<=numbers[right])
                right = mid;
        }
        return numbers[right];
    }
};

按照以上思路写出来的代码还存在问题,中间元素和第一个和最后一个元素都相等的情况下,范围会缩小到后面的递增子序列是不对的,如:[10,1,10,10,10]测试用例

就是中间元素和第一个和最后一个元素都相等时需要在[left, right]范围上顺序查找

3

第一个元素和最后一个元素以及中间元素相等时就需要在[left, right]范围上顺序查找

C++

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int left = 0;
        int right = numbers.size()-1;
        int mid = 0;
        if(numbers[left] < numbers[right]) return numbers[left];
        while(left+1 < right){
            mid = (left+right)>>1;
            if(numbers[left] == numbers[right] && numbers[right] == numbers[mid]){ //顺序查找
                int min = numbers[left];
                for(int i=left+1; i<=right; ++i)
                    if(min > numbers[i])
                        min = numbers[i];
                return min;
            }
            else if(numbers[mid]>=numbers[left])
                left = mid;
            else if(numbers[mid]<=numbers[right])
                right = mid;
        }
        return numbers[right];
    }
};

C

int minArray(int* numbers, int numbersSize){
    int left = 0;
    int right = numbersSize-1;
    int mid = 0;
    if(numbers[left] < numbers[right]) return numbers[left];
    while(left+1 < right){
        mid = (left+right)>>1;
        if(numbers[left] == numbers[right] && numbers[right] == numbers[mid]){ //顺序查找
            int min = numbers[left];
            for(int i=left+1; i<=right; ++i)
                if(min > numbers[i])
                    min = numbers[i];
            return min;
        }
        else if(numbers[mid]>=numbers[left])
            left = mid;
        else if(numbers[mid]<=numbers[right])
            right = mid;
    }
    return numbers[right];
}

正确,可以通过所有的测试用例

4

封装顺序查找函数(封装函数之后没有直接顺序查找分支选择执行用时短)

class Solution {
public:
    int minArray(vector<int>& numbers) {
        int left = 0;
        int right = numbers.size()-1;
        int mid = 0;
        if(numbers[left] < numbers[right]) return numbers[left];
        while(left+1 < right){
            mid = (left+right)>>1;
            if(numbers[left] == numbers[right] && numbers[right] == numbers[mid]) //顺序查找
                return seqSearch(numbers, left, right);
            else if(numbers[mid]>=numbers[left])
                left = mid;
            else if(numbers[mid]<=numbers[right])
                right = mid;
        }
        return numbers[right];
    }
    int seqSearch(vector<int>& numbers, int left, int right){
        int min = numbers[left];
        for(int i=left+1; i<=right; ++i)
            if(min > numbers[i])
                min = numbers[i];
        return min;
    }
};
思路总结

1. 当第一个元素小于最后一个元素时,说明整个数组递增有序,直接返回第一个元素

2. 找到数组的中间元素,如果该中间元素位于前面的递增子数组,则中间元素一定大于等于第一个指针指向的元素,最小元素就位于该中间元素后面,第一个指针指向该中间元素;如果该中间元素位于后面的递增数组,则中间元素小于等于最后一个指针指向的元素,此时最小元素就位于该中间元素的前面,第二个指针指向该中间元素

3. 第一个元素和最后一个元素以及中间元素相等时就需要在[left, right]范围上顺序查找

面试题9 :斐波那契数列

先来个递归O(N^2)

class Solution {
public:
    int Fibonacci(int n) {
        if(n == 0 || n == 1)return n;
        else
            return Fibonacci(n-1)+Fibonacci(n-2);
    }
};

动归O(N)

请参考动态规划专题的:Fibonacci文章

求斐波那契数列转化为求矩阵的n次方

数学归纳法证明的公式:

[ f ( n ) f ( n − 1 ) f ( n − 1 ) f ( n − 2 ) ] = [ 1 1 1 0 ] n − 1 \begin{bmatrix} f(n) & f(n-1) \\ f(n-1) & f(n-2) \end{bmatrix}= \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n-1} [f(n)f(n−1)​f(n−1)f(n−2)​]=[11​10​]n−1

通过以上公式只需要求

[ 1 1 1 0 ] n − 1 \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{n-1} [11​10​]n−1

即可求得f(n)

代码实现O(n)
class Solution {
public:
    int Fibonacci(int n) {
        if (!n) return 0;
        if (1 == n) return 1;
        if (2 == n) return 1;
        //矩阵a
        vector<vector<int>> a(2, vector<int>(2, 1));
        a[1][1] = 0;
        vector<vector<int>> a_tmp(2, vector<int>(2, 1));
        a_tmp[1][1] = 0;
        for (int i = 1; i < n-2; ++i) {
                int res_0_0 = a_tmp[0][0] * a[0][0] + a_tmp[0][1] * a[1][0];
                int res_0_1 = a_tmp[0][0] * a[0][1] + a_tmp[0][1] * a[1][1];
                int res_1_0 = a_tmp[1][0] * a[0][0] + a_tmp[1][1] * a[1][0];
                int res_1_1 = a_tmp[1][0] * a[0][1] + a_tmp[1][1] * a[1][1];
                a_tmp[0][0] = res_0_0;
                a_tmp[0][1] = res_0_1;
                a_tmp[1][0] = res_1_0;
                a_tmp[1][1] = res_1_1;
        }
        return a_tmp[0][0] + a_tmp[1][0];
    }
};

以上时间复杂度依然是O(n),因此可以使用乘方的性质

a n = { a n / 2 ∗ a n / 2 if  n 为 偶 数 a ( n − 1 ) / 2 ∗ a ( n − 1 ) / 2 ∗ a if  n 为 奇 数 a^n = \begin{cases} a^{n/2}*a^{n/2} &\text{if } n为偶数 \\ a^{(n-1)/2}*a^{(n-1)/2}*a &\text{if } n为奇数 \end{cases} an={an/2∗an/2a(n−1)/2∗a(n−1)/2∗a​if n为偶数if n为奇数​

想要得到n次方,就要先求n/2次方,再把n/2次方的结果平方一下即可,递归方式时间复杂度O(logn)的方式

代码实现O(logn)

Java

/*
	 * O(logN)解法:由f(n) = f(n-1) + f(n-2),可以知道
	 * [f(n),f(n-1)] = [f(n-1),f(n-2)] * {[1,1],[1,0]}
	 * 所以最后化简为:[f(n),f(n-1)] = [1,1] * {[1,1],[1,0]}^(n-2)
	 * 所以这里的核心是:
	 * 1.矩阵的乘法
	 * 2.矩阵快速幂(因为如果不用快速幂的算法,时间复杂度也只能达到O(N))
	 */
public class Solution {
    public int Fibonacci(int n) {
		if (n < 1) {
			return 0;
		}
		if (n == 1 || n == 2) {
			return 1;
		}
		//底
		int[][] base = {{1,1},
						{1,0}};
		//求底为base矩阵的n-2次幂
		int[][] res = matrixPower(base, n - 2);
		//根据[f(n),f(n-1)] = [1,1] * {[1,1],[1,0]}^(n-2),f(n)就是
		//1*res[0][0] + 1*res[1][0]
		return res[0][0] + res[1][0];
    }
    
	//矩阵乘法
	public int[][] multiMatrix(int[][] m1,int[][] m2) {
		//参数判断什么的就不给了,如果矩阵是n*m和m*p,那结果是n*p
		int[][] res = new int[m1.length][m2[0].length];
		for (int i = 0; i < m1.length; i++) {
			for (int j = 0; j < m2[0].length; j++) {
				for (int k = 0; k < m2.length; k++) {
					res[i][j] += m1[i][k] * m2[k][j];
				}
			}
		}
		return res;
	}
	/*
	 * 矩阵的快速幂:
	 * 1.假如不是矩阵,叫你求m^n,如何做到O(logn)?答案就是整数的快速幂:
	 * 假如不会溢出,如10^75,把75用用二进制表示:1001011,那么对应的就是:
	 * 10^75 = 10^64*10^8*10^2*10
	 * 2.把整数换成矩阵,是一样的
	 */
	public int[][] matrixPower(int[][] m, int p) {
		int[][] res = new int[m.length][m[0].length];
		//先把res设为单位矩阵
		for (int i = 0; i < res.length; i++) {
			res[i][i] = 1;
		} //单位矩阵乘任意矩阵都为原来的矩阵
		//用来保存每次的平方
		int[][] tmp = m;
		//p每循环一次右移一位
		for ( ; p != 0; p >>= 1) {
			//如果该位不为零,应该乘
			if ((p&1) != 0) {
				res = multiMatrix(res, tmp);
			}
			//每次保存一下平方的结果
			tmp = multiMatrix(tmp, tmp);
		}
		return res;
	}
	
}

C++

/*
	 * O(logN)解法:由f(n) = f(n-1) + f(n-2),可以知道
	 * [f(n),f(n-1)] = [f(n-1),f(n-2)] * {[1,1],[1,0]}
	 * 所以最后化简为:[f(n),f(n-1)] = [1,1] * {[1,1],[1,0]}^(n-2)
	 * 所以这里的核心是:
	 * 1.矩阵的乘法
	 * 2.矩阵快速幂(因为如果不用快速幂的算法,时间复杂度也只能达到O(N))
	 */
class Solution {
    public:
    int Fibonacci(int n) {
		if (n < 1) {
			return 0;
		}
		if (n == 1 || n == 2) {
			return 1;
		}
		//底
        vector<vector<int>> base(2, vector<int>(2, 1));
        base[1][1] = 0;
		//求底为base矩阵的n-2次幂
        vector<vector<int>>* res;
		res = matrixPower(base, n - 2);
		//根据[f(n),f(n-1)] = [1,1] * {[1,1],[1,0]}^(n-2),f(n)就是
		//1*res[0][0] + 1*res[1][0]
		return (*res)[0][0] + (*res)[1][0];
    }
    
	//矩阵乘法
	vector<vector<int>>* multiMatrix(vector<vector<int>> m1,vector<vector<int>> m2) {
		//参数判断什么的就不给了,如果矩阵是n*m和m*p,那结果是n*p
		vector<vector<int>>* res = new vector<vector<int>>(m1.size(), vector<int>(m2[0].size(), 0));
		for (int i = 0; i < m1.size(); i++) {
			for (int j = 0; j < m2[0].size(); j++) {
				for (int k = 0; k < m2.size(); k++) {
					(*res)[i][j] += m1[i][k] * m2[k][j];
				}
			}
		}
		return res;
	}
	/*
	 * 矩阵的快速幂:
	 * 1.假如不是矩阵,叫你求m^n,如何做到O(logn)?答案就是整数的快速幂:
	 * 假如不会溢出,如10^75,把75用用二进制表示:1001011,那么对应的就是:
	 * 10^75 = 10^64*10^8*10^2*10
	 * 2.把整数换成矩阵,是一样的
	 */
	vector<vector<int>>* matrixPower(vector<vector<int>> m, int p) {
		vector<vector<int>>* res = new vector<vector<int>>(m.size(), vector<int>(m[0].size(), 0));
		//先把res设为单位矩阵
		for (int i = 0; i < (*res).size(); i++) {
			(*res)[i][i] = 1;
		} //单位矩阵乘任意矩阵都为原来的矩阵
		//用来保存每次的平方
		vector<vector<int>>* tmp = &m;
		//p每循环一次右移一位
		for ( ; p != 0; p >>= 1) {
			//如果该位不为零,应该乘
			if ((p&1) != 0) {
				res = multiMatrix(*res, *tmp);
			}
			//每次保存一下平方的结果
			tmp = multiMatrix(*tmp, *tmp);
		}
		return res;
	}
};

面试题10 :二进制中1的个数

移位判断

int hammingWeight(uint32_t n) {
    char res = 0;
    while(n){
        res += n&1;
        n >>= 1;
    }
    return res;
}

缺陷:负数就会死循环–>改进:

不对原数据进行移位,而是使用一个无符号整形不断左移去试探n的每一位

int hammingWeight(uint32_t n) {
    uint32_t flag = 1;
    char res = 0;
    while(flag){
        if(n&flag)res++;
        flag <<= 1;
    }
    return res;
}

更高效率做法

现象:将一个数减1,则原数最低位1会变成0,其后的所有0都变成0

规律:将一个数减1的值和它本身与运算就会将最低非0位的1变成0

以上规则以循环方式进行,有多少个1就能执行多少次

int hammingWeight(uint32_t n) {
    char res = 0;
    while(n){
        n = n&(n-1);
        ++res;
    }
    return res;
}

在leetcode上0ms就挺惊喜

面试题11 :数值的整数次方

无脑循环

double myPow(double x, int n){
    double res = 1.0;
    for(int i=1; i<=n; ++i)
        res *= x;
    return res;
}

没有考虑指数是0或者负数的情况,改进:

double myPow(double x, int n){
    if(!n) return 1.0;
    int flag = 0; //标志指数n是不是负数
    if(n<0){
        flag = 1;
        n *= -1;
    }
    double res = 1.0;
    for(int i=1; i<=n; ++i)
        res *= x;
    if(flag) return 1.0/res;
    return res;
}
0.00001
2147483647

测试用例过不了,因为计算机表示数字存在误差,当两个数相差小于0.0000001时就认为两数相等,那么当res是0.0000001时,就认为res是0.0。封装equal函数用于比较两个双精度浮点数是否相等。

bool equal(double x, double y) {
	if (x - y > -0.0000001 && x - y < 0.0000001)
		return true;
	else
		return false;
}
double myPow(double x, int n) {
	if (equal(x, 1.0)) return 1.0;
	unsigned int n_tmp = n; //防止n=-2147483648时 n*=-1溢出
	
	//n<0
	if (n < 0) {
		x = 1.0 / x;
		//n *= -1
		n += 1;
		n *= -1;
		n_tmp = n;
		++n_tmp;
	}
	
	//x == -1.0
	if (equal(x, -1.0) && n_tmp % 2) return -1.0;
	if (equal(x, -1.0) && !(n_tmp % 2)) return 1.0;
	double res = 1.0;
	for (int i = 1; i <= n_tmp; ++i) {
		res *= x;
		if (equal(res, 0.0)) return 0.0;
	}
	return res;
}
小小总结
  • x == 1.0 return 1.0
  • x == -1.0 指数是奇数返回-1.0;指数是偶数返回1.0
  • 循环中只要 res <= 0.0000001 && res >= -0.0000001,就return 0.0

使用求斐波那契数列转化为求矩阵的n次方的公式(递归)

a n = { a n / 2 ∗ a n / 2 if  n 为 偶 数 a ( n − 1 ) / 2 ∗ a ( n − 1 ) / 2 ∗ a if  n 为 奇 数 a^n = \begin{cases} a^{n/2}*a^{n/2} &\text{if } n为偶数 \\ a^{(n-1)/2}*a^{(n-1)/2}*a &\text{if } n为奇数 \end{cases} an={an/2∗an/2a(n−1)/2∗a(n−1)/2∗a​if n为偶数if n为奇数​

double myPow(double x, int n){
    if(0 == n) return 1;
    if(-1 == n) return 1/x;
    double res = myPow(x, n>>1);
    res *= res;
    if(n & 1) 
        res *= x;
    return res;
}
double myPow(double x, int n) {
    if(n==0) return 1;
    //已经考虑到负数右移永远是负数的情况
    if(n==-1) return 1/x;
    if(n&1) return myPow(x*x, n>>1)*x;
    else return myPow(x*x, n>>1);
}

上面两种递归,1>>1最终都是0,因此可以不用给if(1 == n) return x;的递归结束分支,给的话递归会少一层效率响应时间可能会有所提高

bool equal(double x, double y) {
	if (x - y > -0.0000001 && x - y < 0.0000001)
		return true;
	else
		return false;
}
double myPow(double x, int n) {
    if(equal(x, 0.0)) return 0;
    if(n==0) return 1;
    if(n==1) return x;
    if(n== -1) return 1/x;
    double half = myPow(x,n>>1);
    double mod = myPow(x,n&1);
    return half*half*mod;
}

以上递归if(n == 1) return x分支不能少,删除这个分支,n == 1时mod值就无法计算

快速幂(迭代)

double myPow(double x, int n) {
    double res=1;
    double base=x;
    bool flag=n>=0;
    //负数取反,考虑到最小负数,需要先自增,后续再除以2
    if(!flag) n=-(++n);
    while(n>0){
        if(n&1) res*=x;
        n=n>>1;
        x*=x;
    }
    return flag?res:1/(res*base);
}
double myPow(double x, int n) {
    double res = 1.0;
    int t = n;
    while(n){
        if(n&1) res *= x;
        x *= x;
        n /= 2;
    }
    return t > 0? res : 1.0 / res;
}

快速幂精讲

。。。

面试题12 :打印1到最大的n位数

无脑循环

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* printNumbers(int n, int* returnSize){
    *returnSize = 9;
    for(int i=0; i<n-1; ++i)
        *returnSize = (*returnSize)*10 + 9;
    int* res = (int*)malloc(sizeof(int)*(*returnSize));
    for(int i=0; i<(*returnSize); ++i)
        res[i] = i+1;
    return res;
}

使用pow函数

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* printNumbers(int n, int* returnSize){
    *returnSize = pow(10, n)-1;
    int* res = (int*)malloc(sizeof(int)*(*returnSize));
    for(int i=0; i<(*returnSize); ++i)
        res[i] = i+1;
    return res;
}

在字符串上模拟数字加法

class Solution {
public:
    //字符串模拟数字累加
    string* increase(string* des) {
        int i = des->size() - 1;
        int cur = (*des)[i] - '0' + 1;
        if(1 == des->size())
            (*des)[i] = cur % 10 + '0';
        for (i; i > 0; --i) {
            if (cur == 0) break;
            (*des)[i - 1] += cur / 10;
            (*des)[i] = cur % 10 + '0';
            cur = (*des)[i - 1]-'0';
        }
        return des;
    }
    //返回删除字符串前面0的字符串
    string removeZero(string* des) {
        string res;
        int i = 0;
        while ((*des)[i] == '0')++i;
        for (i; i < (*des).size(); i++)
                res += (*des)[i];
        return res;
    }
    vector<int> printNumbers(int n) {
        vector<int> res;
        int nums = pow(10, n) - 1;
        string tmp(n, '0');
        for (int i = 1; i <= nums; ++i)
            res.push_back(std::atoi(removeZero(increase(&tmp)).c_str()));
        return res;
    }
};

转化为数字排列问题(回溯)

递归

class Solution {
public:
// 此题在剑指offer上主要练习大数问题,P116
    vector<int> result;
    vector<int> printNumbers(int n) {
        if(n <= 0) {
            return {};
        }
        string number(n, '0');
        for(int i = 0; i < 10; ++i) {
            number[0] = i + '0';
            CombinateRecursively(number, n, 0);
        }
        return result;
    }

    void CombinateRecursively(string& number, int nLength, int index) {
        if(index == nLength - 1) {
            SaveNumber(number);
            return;
        }
        for(int i = 0; i < 10; ++i) {
            number[index + 1] = i + '0';
            CombinateRecursively(number, nLength, index + 1);
        }
    }
    void SaveNumber(string& number) {
        bool isBeginning0 = true;
        int nLength = number.size();
        int sumTmp = 0;
        for(int i = 0; i < nLength; ++i) {
            if(isBeginning0 && number[i] != '0') {
                isBeginning0 = false;
            } 
            if(!isBeginning0) {
                sumTmp = sumTmp * 10 + number[i] - '0';
            }
        }
        if(sumTmp != 0) result.push_back(sumTmp);
    }
};

回溯

class Solution {
    /*
    回溯,用字符串的方式求n位数之内的全排列,回溯的时候需要遍历到每个数字,且需要一个列表保存每次dfs触底生成的数字,所以时空复杂度均为O(10^n)
    */
    public int[] printNumbers(int n) {
        List<Character> path = new ArrayList<>();
        List<String> ansList = new ArrayList<>();
        this.dfs(n, 0, ansList, path);
        int[] ans = new int[ansList.size()];
        for(int i = 0; i < ansList.size(); i++){
            ans[i] = Integer.valueOf(ansList.get(i));
        }
        return ans;
    }

    private void dfs(int n, int  depth, List<String> ansList, List<Character> path){
        //此时构建字符串形式的数字
        if(depth == n){
            StringBuilder sb = new StringBuilder();
            boolean flag = false;
            for(int i = 0; i < n; i++){
                Character c = path.get(i);

                //忽略字符串中的前导0字符
                if(flag || !c.equals('0')){
                    flag = true;
                    sb.append(c);
                }
            }

            //全是0字符组成的,跳过
            if(!flag){
                return;
            }
            //将有效字符串放到列表里面
            String sNum = sb.toString();
            ansList.add(sNum);
            return ;
        }
        for(int i = 0; i < 10; i++){
            //当前路径中添加当前数字的字符形式;
            path.add(String.valueOf(i).charAt(0));
            dfs(n, depth+1, ansList, path);
            //回溯
            path.remove(path.size()-1);
        }
    }
}

回溯与全排列

。。。

面试题13 :在O(1)时间删除链表结点

顺序查找到要删除节点的前一个然后删除O(n)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* deleteNode(ListNode* head, int val) {
        ListNode* cur = head;
        if(cur!=nullptr && cur->val == val)
            head = cur->next;
        while(cur!=nullptr){
            if(cur->next!=nullptr && cur->next->val == val)
                break;
            cur = cur->next;
        }
        if(cur)
            cur->next = cur->next->next;
        return head;
    }
};

力扣上只能遍历找到要删除节点的前一个节点再进行删除

将要删除的节点的下一个节点的值覆盖要删除的节点,然会删除要删除节点位置的下一个节点O(1)

deleteNode函数就是删除节点的时间复杂度是O(1)的算法

#include <iostream>
#include <vector>
using namespace std;

typedef struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
}ListNode;
/*
head = [4,5,1,9], val = 5
*/
//删除节点核心函数
ListNode* deleteNode(ListNode* head, ListNode* del) {
	//del不是最后一个节点,-->O(1)删除方式
	if (del->next) {
		del->val = del->next->val;
		del->next = del->next->next;
		return head;
	}
	//del已经是最后一个节点了,只能遍历找到del的前一个节点然后删除-->退变为O(n)方式
	ListNode* cur = head;
	while (cur != nullptr) {
		if (cur->next != nullptr && cur->next == del)
			break;
		cur = cur->next;
	}
	if (cur)
		cur->next = cur->next->next;
	return head;
}
//通过vector构造链表
ListNode* constructList(vector<int> set) {
	ListNode* head = nullptr;
	ListNode* cur = nullptr;
	for (int i = 0; i < set.size(); ++i) {
		ListNode* tmp = (ListNode*)malloc(sizeof(ListNode));
		tmp->val = set[i];
		tmp->next = nullptr;
		if (!i)
			cur = head = tmp;
		else {
			cur->next = tmp;
			cur = tmp;
		}
	}
	return head;
}
//找到要删除的节点并返回
ListNode* findDel(ListNode* head, int val) {
	ListNode* tmp = head;
	while (tmp != nullptr) {
		if (tmp->val == val)break;
		tmp = tmp->next;
	}
	if (tmp)
		return tmp;
}
//遍历链表
void showList(ListNode* head) {
	ListNode* cur = head;
	while (cur) {
		cout << cur->val << "-->";
		cur = cur->next;
	}
	cout <<"null"<< endl;
}
void test() {
	vector<int> set = {4, 5, 1, 9};
	//构造4->5->1->9->nullptr
	ListNode* head = constructList(set);
	showList(head);
	//删除5
	head = deleteNode(head, findDel(head, 5));
	showList(head);
	head = deleteNode(head, findDel(head, 4));
	showList(head);
	head = deleteNode(head, findDel(head, 9));
	showList(head);
}
int main() {
	test();
	return 0;
}

面试题14 :调整数组顺序使奇数位于偶数前面

使用辅助空间划分,然后合并O(n)

使用两个辅助数组,遍历一遍原数组,将偶数和奇数分别放入两个数组,按奇前偶后合并回原数组,时间复杂度O(n),不做代码展示…

双指针一次循环搞定O(n)

第一个指针从前往后走,遇到第一个偶数停止;第二个指针从后往前走,遇到第一个奇数停止。两者交换。

只要第一个指针在第二个指针前面,按以上规则循环起来,就可以调整数组顺序使奇数位于偶数前面

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        int first = 0, last = nums.size() - 1;
        while (first < last-1) {
            //first:当前状态下从前往后第一个偶数
            while (first < nums.size() && nums[first] % 2 != 0) ++first;
            //last:当前状态下从后往前第一个奇数
            while (last > 0 && nums[last] % 2 == 0) --last;
            //first >= last:说明已经奇前偶后
            //first或者last已经超出数组边界,说明数组全是奇数或者偶数则不需要任何交换
            if (first >= nums.size() || last < 0 || first >= last)break;
            int tmp = nums[first];
            nums[first] = nums[last];
            nums[last] = tmp;
            //位操作的交换效率可能高一点
            // nums[first] ^= nums[last];
            // nums[last] ^= nums[first];
            // nums[first] ^= nums[last];
        }
        return nums;
    }
};

我们将两个嵌套的while的判断封装成函数,这样我们只需要再封装不同的判断规则,替换两个嵌套while的判断部分就可以将数组按任何合理存在的有效的规则划分成两部分。例如:数组中按正负分(正前负后)、能否整除3分(整除在前,不能在后)

这个点就是剑指Offer提到的精髓部分

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        int first = 0, last = nums.size() - 1;
        while (first < last-1) {
            //first:当前状态下从前往后第一个偶数
            while (first < nums.size() && !isEven(nums[first])) ++first;
            //last:当前状态下从后往前第一个奇数
            while (last > 0 && isEven(nums[last])) --last;
            //first >= last:说明已经奇前偶后
            //first或者last已经超出数组边界,说明数组全是奇数或者偶数则不需要任何交换
            if (first >= nums.size() || last < 0 || first >= last)break;
            // int tmp = nums[first];
            // nums[first] = nums[last];
            // nums[last] = tmp;
            //位操作的交换效率可能高一点
            nums[first] ^= nums[last];
            nums[last] ^= nums[first];
            nums[first] ^= nums[last];
        }
        return nums;
    }
    //判断是不是偶数
    bool isEven(int num){
        return !(num % 2);
    }
};

面试题15 :链表中倒数第k个结点

单向链表所以注定不能从链表尾回溯k步得到目标节点

利用栈O(n)

遍历一遍链表依次全部入栈(入节点地址),出栈k次得到节点地址

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode* res = nullptr;
        stack<ListNode*> st;
        while(head){
            st.push(head);
            head = head->next;
        }
        while(k){
            res = st.top();
            st.pop();
            --k;
        }
        return res;
    }
};

在力扣中,响应时间还算可以接受(4ms);耗费内存真心不小10.5MB

利用栈思想手动优化,我们最终只要得到倒数k个栈定元素就可以了,因此我们可以用k个元素的数组(使数组逻辑上是循环的)去模拟压栈出栈这个过程

循环数组模拟栈(数组在堆空间动态开辟)O(n)

循环数组的好处是,不用出栈的步骤,直接返回逻辑数组最后一个元素的下一个元素就是倒数第K个节点的地址

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        // ListNode** arr_stack = (ListNode**)malloc(sizeof(ListNode*)*k);
        ListNode** arr_stack = new ListNode*[k]();
        auto_ptr<ListNode**> ap = arr_stack;
        int i = 0;
        while(head){
            arr_stack[i] = head;
            i = (i+1) % k; //arr_stack在逻辑上是循环的
            head = head->next;
        }
        return arr_stack[i];
    }
};

响应时间和内存消耗都与上面代码持平,当数据量多的时候,当前代码一定比上面代码内存消耗少

存在改进的点就是arr_stack的堆空间我们应该用智能指针维护,或者在return前用临时变量记录一下结果手动delete空间:

在return前用临时变量记录一下结果手动delete空间
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode* res = nullptr;
        ListNode** arr_stack = new ListNode*[k]();
        int i = 0;
        while(head){
            arr_stack[i] = head;
            i = (i+1) % k; //arr_stack在逻辑上是循环的
            head = head->next;
        }
        res = arr_stack[i];
        delete[] arr_stack;
        return res;
    }
};
智能指针维护堆空间

模拟实现智能指针维护堆空间

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
template <class T>
class selef_auto_ptr {
public:
	selef_auto_ptr(T* spa) { this->space = spa; }
	~selef_auto_ptr() {
		delete[] this->space;
	}
private:
	T* space;
};
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        // ListNode** arr_stack = new ListNode*[k]();
        ListNode** arr_stack = new ListNode * [k]();
        selef_auto_ptr<ListNode*> ap(arr_stack);
        int i = 0;
        while (head) {
            arr_stack[i] = head;
            i = (i + 1) % k; //arr_stack在逻辑上是循环的
            head = head->next;
        }
        return arr_stack[i];
    }
};

双指针法O(n)

让两个指针保持k-1距离的指针以相同的速度遍历链表,当靠后的指针走到最后一个节点就停止,此时前面的指针指向了倒数第k个节点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode* first = head;
        ListNode* last = head;
        int tmp = k;
        while(--tmp)
            last = last->next;
        while(last->next){
            first = first->next;
            last = last->next;
        }
        return first;
    }
};

双指针法一定比借助后进先出数据结构省内存消耗,也就是空间复杂度低

面试题16 :反转链表

指针法

原链表不断头删,新链表不断头插

原链表需要的指针:head(指向链表头)、tmp(用于指向头删的节点)
新链表需要的指针:newHead(用于维护新链表–>头插,永远指向新链表第一个节点)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* newHead = nullptr;
        while(head){
            ListNode* tmp = head;
            head = head->next;
            if(!newHead){
                newHead = tmp;
                newHead->next = nullptr;
            }
            else{
                tmp->next = newHead;
                newHead = tmp;
            }
        }
        return newHead;
    }
};

分析以上代码:

当给定的原链表为空,返回的依然是空–>正确
当给定的原链表只有一个节点–>正确

递归

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode* node = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return node;
    }                    
};

面试题17 :合并两个排序的链表

遍历链表直接合并

同时遍历两个链表,取两个链表中最小值的节点尾插到结果链表,当一个链表走到尾时,循环结束。将非空连接到结果链表尾就合并了两个排序链表了

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode* tmp = nullptr;
        ListNode* res = nullptr;
        ListNode* res_end = nullptr;
        //取两个链表中最小值的节点尾插到结果链表
        while(l1 && l2){
            if(l1->val <= l2->val){
                tmp = l1;
                l1 = l1->next;
            }else{
                tmp = l2;
                l2 = l2->next;
            }
            if(!res)
                res = res_end = tmp;
            else{
                res_end->next = tmp;
                res_end = tmp;
            }
        }
        //有一个链表是空了就将非空连接到结果链表尾
        if(l1){
            if(!res)
                res = l1;
            else
                res_end->next = l1;
        }else{
            if(!res)
                res = l2;
            else
                res_end->next = l2;
        }
        return res;
    }
};

代码精简以下如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode* tmp = nullptr;
        ListNode* res = nullptr;
        ListNode* res_end = nullptr;
        //取两个链表中最小值的节点尾插到结果链表
        while(l1 && l2){
            tmp = l1->val <= l2->val ? l1 : l2;
            tmp == l1 ? l1 = l1->next : l2 = l2->next;
            res_end = !res ? res = tmp : res_end->next = tmp;
        }
        //有一个链表是空了就将非空连接到结果链表尾
        if(l1)
            !res ? res = l1 : res_end->next = l1;
        else
            !res ? res = l2 : res_end->next = l2;
        return res;
    }
};

面试题18 :树的子结构

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:
给定的树 A:

     3
    / \
   4   5
  / \
 1   2

给定的树 B:

   4 
  /
 1

返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

思路

这道题思路其实很简单,只需要遍历树A的所有节点,只有与树B的根节点相同则进行第二部判断,判断树B能否在树A中其它子节点值也相同

递归思路比较容易,先看递归解法

递归
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        bool result = false;
        if(A && B){
            if(A->val == B->val)
                result = doesTree1HaveTree2(A, B);
            if(!result)
                result = isSubStructure(A->left, B);
            if(!result)
                result = isSubStructure(A->right, B);
        }
        return result;
    }
    bool doesTree1HaveTree2(TreeNode* Tree1, TreeNode* Tree2){
        if(!Tree2)
            return true;
        if(!Tree1)
            return false;
        if(Tree1->val != Tree2->val)
            return false;
        return doesTree1HaveTree2(Tree1->left, Tree2->left) && doesTree1HaveTree2(Tree1->right, Tree2->right);
    }

};
迭代

借助队列结构实现树的层次遍历,一旦树B的根节点在树A中找到,就判断树B其它节点是否也在树A中

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(!B) return false;
        vector<TreeNode*> mid_res_set = levelTraversal(A, B->val); //在A中找到所有与B根节点值相同的节点
        for(int i=0; i<mid_res_set.size(); ++i){
            if(source_has_des(mid_res_set[i], B)) return true;
        }
        return false;
    }
    //借助队列结构实现树的层次遍历,返回找到节点值的数组
    vector<TreeNode*> levelTraversal(TreeNode* des, int find_val){
        //空树,直接返回空数组
        if(!des) return {};
        //队列
        queue<TreeNode*> qu;
        qu.push(des);
        //结果数组
        vector<TreeNode*> res;
        //保存层次遍历顺序的节点
        TreeNode* element;
        //层次遍历
        while(!qu.empty()){
            element = qu.front();
            if(element->val == find_val)
                res.push_back(element);
            qu.pop(); //出队一个根节点
            //入队这个根的两个子节点
            if(element->left) qu.push(element->left);
            if(element->right) qu.push(element->right);
        }
        return res;
    }
    //同时前序遍历des和source判断des是不是在source中能找到:若是一旦有节点不相同就返回假,遍历完了就返回真
    bool source_has_des(TreeNode* source, TreeNode* des){
        stack<TreeNode*> st_des;
        stack<TreeNode*> st_source;
        TreeNode* pNode_des = des;
        TreeNode* pNode_source = source;
        while (pNode_des != nullptr || !st_des.empty()) {
            if (pNode_des != nullptr) {
                if(!pNode_source) return false;
                if(pNode_des->val != pNode_source->val)
                    return false;
                st_des.push(pNode_des);
                st_source.push(pNode_source);
                pNode_des = pNode_des->left;
                pNode_source = pNode_source->left;
            }
            else {
                TreeNode* node_des = st_des.top();
                TreeNode* node_source = st_source.top();
                st_des.pop();
                st_source.pop();
                pNode_des = node_des->right;
                pNode_source = node_source->right;
            }
        }
        return true;
    }
};

二叉树的遍历之递归和非递归的广度、深度优先遍历

广度优先搜索BFS(宽度优先搜索,或横向优先搜索)

也就是二叉树的层次遍历

利用队列结构:

初始状态:顶级根节点入队

队列不空就循环出队,出队的节点尾插入vector,出一个节点就入出队的节点的左节点和右节点(没有左或者右就不入)

循环结束vector就记录了层次遍历的所有节点
//层次遍历(利用队列)
vector<TreeNode*> levelTraverse(TreeNode* des) {
	//空树,直接返回空数组
	if (!des) return {};
	//队列
	queue<TreeNode*> qu;
	qu.push(des);
	//结果数组
	vector<TreeNode*> res;
	//保存层次遍历的节点
	TreeNode* element;
	//层次遍历
	while (!qu.empty()) {
		element = qu.front();
		res.push_back(element);
		qu.pop(); //出队一个根节点
		//入队这个根的两个子节点(先左后右)
		if (element->left) qu.push(element->left);
		if (element->right) qu.push(element->right);
	}
	return res;
}

深度优先搜索DFS:前序、中序、后续遍历

前序
递归
void preOrderTraverse1Recursion(TreeNode* des, vector<TreeNode*>& res) {
	if (des) {
		res.push_back(des);
		preOrderTraverse1Recursion(des->left, res);
		preOrderTraverse1Recursion(des->right, res);
	}
}
非递归

方法1

a. 初始状态:整个树的顶级根节点先入栈
    
b. 栈非空就循环出栈,每层循环出栈的节点的右子节点和左子节点依次入栈(右或者左为空就不入)
//前序遍历(循环、利用栈)
/*
初始状态:整个树的顶级根节点先入栈

栈非空就循环出栈,每层循环出栈的节点的右子节点和左子节点依次入栈(右或者左为空就不入)
*/
vector<TreeNode*> preOrderTraverse(TreeNode* des) {
	//空树,直接返回空数组
	if (!des) return {};
	//栈
	stack<TreeNode*> st;
	st.push(des);
	//结果数组
	vector<TreeNode*> res;
	//保存前序遍历的节点
	TreeNode* element;
	//层次遍历
	while (!st.empty()) {
		element = st.top();
		res.push_back(element);
		st.pop(); //出队一个根节点
		//入栈这个根的两个子节点(先右后左)
		if (element->right) st.push(element->right);
		if (element->left) st.push(element->left);
	}
	return res;
}

方法2

根据前序遍历的顺序,优先访问根结点,然后在访问左子树和右子树。所以,对于任意结点node,第一部分即直接访问之,
之后在判断左子树是否为空,不为空时即重复上面的步骤,直到其为空。若为空,则需要访问右子树。
注意,在访问过左孩子之后,需要反过来访问其右孩子,所以,需要栈这种数据结构的支持。对于任意一个结点node,具体步骤如下:

a. 访问之,并把结点node入栈,当前结点置为左孩子;

b. 判断结点node是否为空,若为空,则取出栈顶结点并出栈,将右孩子置为当前结点;
否则重复a)步直到当前结点为空或者栈为空(可以发现栈中的结点就是为了访问右孩子才存储的)
//前序遍历(循环、利用栈)
/*
根据前序遍历的顺序,优先访问根结点,然后在访问左子树和右子树。所以,对于任意结点node,第一部分即直接访问之,
之后在判断左子树是否为空,不为空时即重复上面的步骤,直到其为空。若为空,则需要访问右子树。
注意,在访问过左孩子之后,需要反过来访问其右孩子,所以,需要栈这种数据结构的支持。对于任意一个结点node,具体步骤如下:

a)访问之,并把结点node入栈,当前结点置为左孩子;

b)判断结点node是否为空,若为空,则取出栈顶结点并出栈,将右孩子置为当前结点;
否则重复a)步直到当前结点为空或者栈为空(可以发现栈中的结点就是为了访问右孩子才存储的)
*/
vector<TreeNode*> preOrderTraverse2(TreeNode* des) {
	stack<TreeNode*> st;
	vector<TreeNode*> res;
	TreeNode* pNode = des;
	while (pNode != nullptr || !st.empty()) {
		if (pNode != nullptr) {
			res.push_back(pNode);
			st.push(pNode);
			pNode = pNode->left;
		}
		else { //pNode == null && !stack.isEmpty()
			TreeNode* node = st.top();
			st.pop();
			pNode = node->right;
		}
	}
	return res;
}
中序
递归
void inOrderTraverseRecursion(TreeNode* des, vector<TreeNode*>& res) {
	if (des) {
		preOrderTraverse1Recursion(des->left, res);
		res.push_back(des);
		preOrderTraverse1Recursion(des->right, res);
	}
}
非递归
vector<TreeNode*> inOrderTraverse2(TreeNode* des) {
	stack<TreeNode*> st;
	vector<TreeNode*> res; //结果数组
	TreeNode* pNode = des;
	while (pNode != nullptr || !st.empty()) {
		if (pNode != nullptr) {
			st.push(pNode);
			pNode = pNode->left;
		}
		else { //pNode == null && !stack.isEmpty()
			TreeNode* node = st.top();
			st.pop();
			res.push_back(node);
			pNode = node->right;
		}
	}
	return res;
}
后序
递归
void postOrderTraverseRecursion(TreeNode* des, vector<TreeNode*>& res) {
	if (des) {
		postOrderTraverseRecursion(des->left, res);
		postOrderTraverseRecursion(des->right, res);
		res.push_back(des);
	}
}
非递归
vector<TreeNode*> postOrderTraverse2(TreeNode* des) {
	vector<TreeNode*> res;
	if (!des) return res;
	stack<TreeNode*> st;
	TreeNode* pNode = des;
	TreeNode* pre = nullptr;
	while (pNode != nullptr || !st.empty()) {
		if (pNode != nullptr) {
			st.push(pNode);
			pNode = pNode->left;
		}
		else {
			TreeNode* node = st.top();
			st.pop();
			pNode = node->right;
			if (!node->right)
				res.push_back(node);
			else {
				node->right = nullptr;
				st.push(node);
			}
		}
	}
	return res;
}

所有遍历的测试代码如下:

//利用队列实现层次遍历,栈实现前序中序后续遍历
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <vector>
#include <queue>
#include <stack>
using namespace std;
struct TreeNode {
	int val;
	TreeNode* left;
	TreeNode* right;
	TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
//构造如下二叉树
/*
0:
	 3
	/ \
   4   5
  / \
 1  2

1:
	 3
	/ \
   4   5
  / \ / \
 1  2 6  7
*/
TreeNode* constructBinaryTree() {
	TreeNode* tmp, * root;
	root = tmp = new TreeNode(3);
	tmp->right = new TreeNode(5);
//条件编译测试两种树0和1分别对应上面两种树
#if 1
	tmp->right->left = new TreeNode(6);
	tmp->right->right = new TreeNode(7);
#endif
	tmp = tmp->left = new TreeNode(4);
	tmp->right = new TreeNode(2);
	tmp->left = new TreeNode(1);
	return root;
}
//广度优先搜索BFS(宽度优先搜索,或横向优先搜索):层次遍历
//层次遍历(利用队列)
vector<TreeNode*> levelTraverse(TreeNode* des) {
	//空树,直接返回空数组
	if (!des) return {};
	//队列
	queue<TreeNode*> qu;
	qu.push(des);
	//结果数组
	vector<TreeNode*> res;
	//保存层次遍历的节点
	TreeNode* element;
	//层次遍历
	while (!qu.empty()) {
		element = qu.front();
		res.push_back(element);
		qu.pop(); //出队一个根节点
		//入队这个根的两个子节点(先左后右)
		if (element->left) qu.push(element->left);
		if (element->right) qu.push(element->right);
	}
	return res;
}

//深度优先搜索DFS:前序、中序、后续遍历
//前序遍历(递归)
void preOrderTraverse1Recursion(TreeNode* des, vector<TreeNode*>& res) {
	if (des) {
		res.push_back(des);
		preOrderTraverse1Recursion(des->left, res);
		preOrderTraverse1Recursion(des->right, res);
	}
}
//前序遍历(循环、利用栈)
/*
初始状态:整个树的顶级根节点先入栈

栈非空就循环出栈

每层循环出栈的节点的右子节点和左子节点依次入栈(右或者左为空就不入)
*/
vector<TreeNode*> preOrderTraverse(TreeNode* des) {
	//空树,直接返回空数组
	if (!des) return {};
	//栈
	stack<TreeNode*> st;
	st.push(des);
	//结果数组
	vector<TreeNode*> res;
	//保存前序遍历的节点
	TreeNode* element;
	//层次遍历
	while (!st.empty()) {
		element = st.top();
		res.push_back(element);
		st.pop(); //出队一个根节点
		//入栈这个根的两个子节点(先右后左)
		if (element->right) st.push(element->right);
		if (element->left) st.push(element->left);
	}
	return res;
}
//前序遍历(循环、利用栈)
/*
根据前序遍历的顺序,优先访问根结点,然后在访问左子树和右子树。所以,对于任意结点node,第一部分即直接访问之,
之后在判断左子树是否为空,不为空时即重复上面的步骤,直到其为空。若为空,则需要访问右子树。
注意,在访问过左孩子之后,需要反过来访问其右孩子,所以,需要栈这种数据结构的支持。对于任意一个结点node,具体步骤如下:

a)访问之,并把结点node入栈,当前结点置为左孩子;

b)判断结点node是否为空,若为空,则取出栈顶结点并出栈,将右孩子置为当前结点;
否则重复a)步直到当前结点为空或者栈为空(可以发现栈中的结点就是为了访问右孩子才存储的)
*/
vector<TreeNode*> preOrderTraverse2(TreeNode* des) {
	stack<TreeNode*> st;
	vector<TreeNode*> res;
	TreeNode* pNode = des;
	while (pNode != nullptr || !st.empty()) {
		if (pNode != nullptr) {
			res.push_back(pNode);
			st.push(pNode);
			pNode = pNode->left;
		}
		else { //pNode == null && !stack.isEmpty()
			TreeNode* node = st.top();
			st.pop();
			pNode = node->right;
		}
	}
	return res;
}

//中序遍历(递归)
void inOrderTraverseRecursion(TreeNode* des, vector<TreeNode*>& res) {
	if (des) {
		preOrderTraverse1Recursion(des->left, res);
		res.push_back(des);
		preOrderTraverse1Recursion(des->right, res);
	}
}
//中序遍历(循环、利用栈)
/*

*/
vector<TreeNode*> inOrderTraverse2(TreeNode* des) {
	stack<TreeNode*> st;
	vector<TreeNode*> res; //结果数组
	TreeNode* pNode = des;
	while (pNode != nullptr || !st.empty()) {
		if (pNode != nullptr) {
			st.push(pNode);
			pNode = pNode->left;
		}
		else { //pNode == null && !stack.isEmpty()
			TreeNode* node = st.top();
			st.pop();
			res.push_back(node);
			pNode = node->right;
		}
	}
	return res;
}

//后续遍历(递归)
void postOrderTraverseRecursion(TreeNode* des, vector<TreeNode*>& res) {
	if (des) {
		postOrderTraverseRecursion(des->left, res);
		postOrderTraverseRecursion(des->right, res);
		res.push_back(des);
	}
}
//后续遍历(循环、利用栈)
vector<TreeNode*> postOrderTraverse2(TreeNode* des) {
	vector<TreeNode*> res;
	if (!des) return res;
	stack<TreeNode*> st;
	TreeNode* pNode = des;
	TreeNode* pre = nullptr;
	while (pNode != nullptr || !st.empty()) {
		if (pNode != nullptr) {
			st.push(pNode);
			pNode = pNode->left;
		}
		else {
			TreeNode* node = st.top();
			st.pop();
			pNode = node->right;
			if (!node->right)
				res.push_back(node);
			else {
				node->right = nullptr;
				st.push(node);
			}
		}
	}
	return res;
}
/*
* postOrderTraverse2启示自:
res = []
stack = []
while root != None or len(stack) != 0:
	if root != None :
		# 入栈
		stack.append(root)
		# 继续迭代左节点
		root = root.left
	else :
		# 先出栈
		p = stack.pop()
		# 迭代右节点
		root = p.right
		# 右节点为空 访问该节点
		if p.right == None:
			res.append(p.val)
		# 右节点不为空 继续迭代
		else:
		# 将当前节点的右节点置为None 再将其放回stack
			p.right = None
			stack.append(p)
	return res
*/
vector<TreeNode*> postOrderTraverse3(TreeNode* des) {
	vector<TreeNode*> res;
	if (!des) return res;
 	stack<TreeNode*> st;
	TreeNode* pNode = des;
	TreeNode* pre = nullptr;
	while (pNode != nullptr || !st.empty()) {
		if (pNode != nullptr) {
			st.push(pNode);
			pNode = pNode->left;
		}else {
			pNode = st.top();
			st.pop();
			if (nullptr == pNode->right || pNode->right == pre) {
				res.push_back(pNode);
				pre = pNode;
				pNode = nullptr;
			}
			else {
				st.push(pNode);
				pNode = pNode->right;
			}
		}
	}
	return res;
}


//遍历vector
void vector_travere(vector<TreeNode*> v) {
	for (int i = 0; i < v.size(); ++i)
		cout << v[i]->val << " ";
	cout << endl;
}
int main() {
	TreeNode* t = constructBinaryTree();
	//层次遍历
	cout << "层次" << endl;
	vector<TreeNode*> levelTraverseArray = levelTraverse(t);
	vector_travere(levelTraverseArray);
	cout << endl;


	//前序遍历
	cout <<"前序"<< endl;
	vector<TreeNode*> res_preOrder;
	preOrderTraverse1Recursion(t, res_preOrder);
	vector_travere(res_preOrder);

	vector<TreeNode*> preTraverseArray = preOrderTraverse(t);
	vector_travere(preTraverseArray);

	vector<TreeNode*> preTraverseArray2 = preOrderTraverse2(t);
	vector_travere(preTraverseArray2);

	cout << endl;



	//中序遍历
	cout << "中序" << endl;
	vector<TreeNode*> res_inOrder;
	inOrderTraverseRecursion(t, res_inOrder);
	vector_travere(res_inOrder);

	vector<TreeNode*> inOrderTraverseArray = inOrderTraverse2(t);
	vector_travere(inOrderTraverseArray);
	
	cout << endl;


	//后序遍历
	cout << "后序" << endl;
	vector<TreeNode*> res_postOrder;
	postOrderTraverseRecursion(t, res_postOrder);
	vector_travere(res_postOrder);

	vector<TreeNode*> postOrderTraverseArray = postOrderTraverse2(t); //postOrderTraverse2会破坏原二叉树结构
	vector_travere(postOrderTraverseArray);

	t = constructBinaryTree(); //重新构造二叉树
	postOrderTraverseArray = postOrderTraverse3(t);
	vector_travere(postOrderTraverseArray);

	cout << endl;
	

	return 0;
}

面试题19 :二叉树的镜像

。。。

面试题20 :顺时针打印矩阵

这种题目也叫转圈打印矩阵,之前在牛客算法题有遇到

思路可以参考专栏:算法(Java 牛客网)的文章:16.转圈打印矩阵

牛客题目:转圈打印矩阵 个人提交通过代码参考

参考以上Java代码实现C++代码

牛客中判题测试用例没有矩阵是空的情况,C++需要判断矩阵是否是空,是空就返回空数组{},其它代码逻辑与牛客网Java完全一致

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        if(!matrix.size()) return {};
        int row = matrix.size();
        int col = matrix[0].size();
        int left = 0;
        int right = col-1;
        int top = 0;
        int floor = row - 1;
        int count = 0;
        vector<int> res(row*col, 0);
        while(left<right && top<floor){
            //往右
            for(int i=left; i<=right; ++i)
                res[count++] = matrix[top][i];
            top++;
            //往下
            for(int i=top; i<=floor; ++i)
                res[count++] = matrix[i][right];
            right--;
            //往左
            for(int i=right; i>=left; --i)
                res[count++] = matrix[floor][i];
            floor--;
            //往上
            for(int i=floor; i>=top; --i)
                res[count++] = matrix[i][left];
            left++;
        }
        if(left == right) //往下
            for (int i = top; i <= floor; ++i)
                res[count++] = matrix[i][right];
        else if(top == floor) //往右
            for(int i=left; i<=right; ++i)
                res[count++] = matrix[top][i];
        return res;
    }
};

文章中所有。。。的地方是暂时不会或者做出来了只是思路不太完善。后续题目持续更新中,请点赞+收藏+关注,一起变强



这篇关于剑指Offer(第一版)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程