剑指offer笔记

文章目录

        • 01赋值运算符函数
        • 02单例模式
        • 03找一维数组
        • 04二维数组查找
        • 05替换字符串的空格
        • 06从尾开始输出单向链表
        • 07前序和中序构建二叉树
        • 08找出中序遍历的下一个节点
        • 09两个栈实现队列
        • 09队列如何实现栈
        • 10斐波那契数列
        • 11partition函数
        • 11快速排序
        • 11数字限定在较小范围内的0(n)方法排序
        • 11旋转数组的最小数字
        • 12矩阵中的路径
        • 13机器人的运动范围
        • 14动态规划
        • 14贪婪算法
        • 14剪绳子
        • 15.位运算
        • 15判断整数二进制中1的个数
        • 15一条语句判断一个整数是否是2的整数次方
        • 15输入整数 m 和 n,需要改变m的二进制中多少位才能得到n
        • 15 本章小结
        • 16 数值的整数次方,不考虑大数问题
        • 17打印从1到最大的n位数
        • 18删除链表的节点
        • offer19 正则表达式匹配
        • 20 表示数值的字符串
        • 21调整数组顺序使奇数在偶数前面
        • 22求单向链表的倒数第K个节点(最后一个是倒数第一个)
        • 22求链表的中间节点
        • 23求链表环的入口节点
        • 24反转链表
        • 25合并两个排序的链表
        • 26树的子结构
        • 27二叉树的镜像
        • 28对称的二叉树
        • 29顺时针打印矩阵
        • 30包含min函数的栈
        • 31栈的压入弹出序列
        • 32二叉树的层序遍历(不分行)
        • 32二叉树的层序遍历(分行)
        • 32二叉树层序遍历(之字形)
        • 33判断是否二叉搜索树的后序遍历序列
        • 34二叉树中和为某一值的路径
        • 分治都可以用递归解决
        • 35复杂链表的复制
        • 36二叉搜索树和双向链表
        • 37序列化和反序列化二叉树
        • 38字符串的所有排列
        • 39求数组中出现次数超过一半的数组
        • 40 最小的K个数O(N)解法
        • 41数据流中的中位数
        • 42连续子数组的最大和
        • 43 1~n整数中1出现的次数
        • 44 数字序列中某一位数字
        • 45把数组排成最小的数
        • 46把数字翻译成字符串
        • 47礼物的最大价值
        • 48最长不含重复字符的子字符串
        • 49丑数
        • 50 第一次只出现一次的字符
        • 51统计数组逆序对-归并排序
        • 52两个链表的第一个公共节点
        • 53统计数字在排序数组中出现的次数
        • 53查找0~n-1中缺失的数字
        • 53查找递增数组中数值与下标相等的元素
        • 54 查找二叉搜索树的第K大节点
        • 55 二叉树的深度
        • 55判断是否为平衡二叉树
        • 55树的深度
        • 56数组中只出现一次的两个数字
        • 56数组唯一只出现一次的数字(其他出现了三次)
        • 57和为S的两个数字(在一个递增数组中的)O(N)
        • 57和为S的连续数字序列(从1开始)
        • 58反转单词顺序
        • 58左旋转字符串
        • 59队列的最大值--滑动窗口的最大值
        • 60n个色子的点数之和s的概率
        • 61扑克牌的顺子
        • 62约瑟夫环
        • 63股票的最大利润
        • 64求解1+++n,不能使用循环、判断语句
        • 65不用加减乘除做加法
        • 65不用新变量交换a b的值
        • 66构建乘积数组
        • 67把字符串转为整数
        • 68两个树节点的最低公共祖先
        • 二叉搜索树
        • 有父节点指针的任意树
        • 没有父节点指针的任意树
        • 63股票的最大利润
        • 64求解1+++n,不能使用循环、判断语句
        • 65不用加减乘除做加法
        • 65不用新变量交换a b的值
        • 66构建乘积数组
        • 67把字符串转为整数
        • 68两个树节点的最低公共祖先
        • 二叉搜索树
        • 有父节点指针的任意树
        • 没有父节点指针的任意树

01赋值运算符函数

  1. 返回类型设为引用,return *this,方便连续赋值、避免返回时的副本拷贝

  2. 参数类型设置为常量引用,避免实参到形参调用复制构造函数、不改变传入实例状态

  3. 判断传入参数和当前实例是否为同一个实例

  4. 释放实例自身的内存,避免内存泄漏

    CMyString & operator = (const CMyString & rhs)
    {
        if(this == &rhs) 
            return *this;
        delete[] m_pData;
        m_pData = new char[strlen(rhs->m_pData)+1];
        strcpy(m_pData, rhs->m_pData);
        return *this;
    }
    
  5. 创建临时实例,交换实例,防止new char抛出异常保证异常安全性、临时实例被自动释放避免内存泄漏

    CMyString & operator = (CMyString & rhs)
    {
        if (this == &rhs)
            return *this;
        CMyString temp(rhs);
        char *p = this->m_pData;
        this->m_pData = temp.m_pData;
        temp.m_pData = p;
        return *this;
    }
    

02单例模式

单例类只有一个实例对象,构造函数是私有的,提供一个静态的公有函数创建或获取改静态私有实例。

  1. 懒汉式单例,时间换空间,有线程安全问题,内存泄漏需要使用智能指针解决

    #include 
    #include 
    #include 
    
    using namespace std;
    
    class Singleton
    {
    public:
        typedef shared_ptr Ptr;//智能指针,防止内存泄漏
        static Ptr getInstance()//共有函数、静态
        {
            if (m_ptr_instance == nullptr)//双检锁,减少锁开销、某些架构下可能失效
            {
                if (m_ptr_instance == nullptr)
                {
                    lock_guard lck(m_mtx);//lock_guard锁
                    m_ptr_instance = (Ptr)(new Singleton);
                }
            }
            return m_ptr_instance;
        }
        ~Singleton()
        {
            cout << "destory." << endl;
        }
        //删除拷贝赋值函数、拷贝构造函数
        Singleton1& operator = (const Singleto1&) = delete;
        Singleton1(const Singleton1&) = delete;
    private:
        Singleton()
        {
            cout << "construct." << endl;
        }
        static Ptr m_ptr_instance;
        static mutex m_mtx;
    };
    Singleton::Ptr Singleton::m_ptr_instance = nullptr;
    mutex Singleton::m_mtx;
    int main()
    {
        Singleton::Ptr ptr_s1 = Singleton::getInstance();
        Singleton::Ptr ptr_s2 = Singleton::getInstance();
        return 0;
    }
    
  2. 懒汉式,局部静态变量实现,并发线程会阻塞等待局部静态变量的初始化

    class Singleton1
    {
    public:
        ~Singleton1()
        {
            cout << "Singleton1 destory" << endl;
        }
        //删除拷贝赋值函数、拷贝构造函数
        Singleton1& operator = (const Singleton1&) = delete;
        Singleton1(const Singleton1&) = delete;
        static Singleton1& getInstance()
        {
            static Singleton1 instance;//该静态变量只会在第一次调用的时候初始化
            return instance;
        }
    private:
        Singleton1()
        {
            cout << "Singleton1 construct" << endl;
        }
    };
    
  3. 饿汉式单例,空间换时间,没有线程安全问题

    class Singleton2
    {
    public:
        ~Singleton2()
        {
            cout << "destory" << endl;
        }
        //删除拷贝赋值函数、拷贝构造函数
        Singleton2& operator = (const Singleton2&) = delete;
        Singleton2(const Singleton2&) = delete;
        static Singleton2* getInstance()//共有、static
        {
            return m_ptr_instance;
        }
    private:
        Singleton2() //私有
        {
            cout << "construct" << endl;
        };
        static Singleton2* m_ptr_instance;//私有、static
    };
    Singleton2* Singleton2::m_ptr_instance = (Singleton2*)(new Singleton2);//直接初始化,类定义后立刻生成单例
    

03找一维数组

//二分法核心,升序数组中找到某个数k
bool func(int numbers[], int length, int k)
{
    int start = 0, end = length - 1;//设定start、end初值
    while(end >= start)//退出条件1
    {
        if (end == start)//退出条件2
        {
            if (numbers[end] == k)
                return true;
            else
                return false;
        }
        int middle = start + (end - start) / 2;//查找一半的范围
        if (k >= numbers[start] && k <= numbers[middle])
            end = middle;//边界收缩
        else
            start = middle + 1;//边界收缩
    }
    return false;
}

04二维数组查找

核心,从数组的右上角开始找,如果小于则加行,如果大于则减列

// offer04查找二维数组.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
using namespace std;

bool find_stl(vector> matrix, int rows, int columns, int number)
{
    bool res = false;
    if (rows < 0 || columns < 0)
        return res;
    int row = 0, column = columns - 1;
    while (row < rows && column > 0)
    {
        if (matrix[row][column] == number)
        {
            res = true;
            break;
        }
        else
        {
            if (matrix[row][column] > number)
                column--;
            else
                row++;
        }
    }
    return res;
}

bool find(int* matrix, int rows, int columns, int number)
{
    bool result = false;
    if (matrix == nullptr || rows < 0 || columns < 0)
        return result;
    int row = 0, column = columns - 1;
    while (row < rows && column > 0)
    {
        if (matrix[row * column + column] == number)
        {
            result = true;
            break;
        } 
        else
        {
            if (matrix[row * column + column] > number)
            {
                ++row;
            }
            else
            {
                --column;
            }
        }
    }
    return result;
}
int main()
{
    /*
    int matrix[16], num, cnt = 0;
    while (cin >> num)
    {
        matrix[cnt++] = num;
    }
    cout << find(matrix, 4, 4, 7);
    std::cout << "Hello World!\n";*/
    vector> matrix;
    int num = 0, cnt = 0;
    while (cin >> num)
    {

    }
    return 0;
}

05替换字符串的空格

先遍历出空格数,从尾遍历,p1指向原尾,p2指向新尾

// offer05字符串替换空格.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

void replace(char* string, int length)
{
    if (string == nullptr || length < 0)
        return;
    int blank_cnt = 0, str_len = 0;
    while (string[str_len] != '\0')
    {
        if (string[str_len] == ' ')
            ++blank_cnt;
        ++str_len;
    }
    int new_len = str_len + 2 * blank_cnt;
    if (new_len + 1 > length)
        return;
    int p1 = str_len, p2 = new_len;
    while (p1 >= 0 && p1 < p2)
    {
        if (string[p1] == ' ')
        {
            string[p2] = '0';
            string[p2 - 1] = '2';
            string[p2 - 2] = '%';
            p2 -= 3;
            --p1;
        }
        else
        {
            string[p2] = string[p1];
            --p1;
            --p2;
        }
    }
}

int main()
{
    char string[100] = { 0 };
    const char* str = " we are happy.";
    strcpy_s(string, str);
    replace(string, 100);
    printf("%s\n", string);
    return 0;
    //std::cout << "Hello World!\n";
}

06从尾开始输出单向链表

方法1:将链表指针反向

方法2:使用栈来存储

方法3:使用递归(能用栈的也可以用递归)


#include 
#include 
using namespace std;
struct ListNode
{
    int m_nValue;
    ListNode* m_pNext;
};

void print_linkList(ListNode *pHead)
{
    stack node_stack;
    ListNode* p = pHead;
    while (p != nullptr);
    {
        node_stack.push(p);
        p = p->m_pNext;
    }
    while (!node_stack.empty())
    {
        p = node_stack.top();
        node_stack.pop();
        cout << p->m_nValue << endl;
    }
}

void print_linkList_recursively(ListNode* pHead)
{
    if (pHead == nullptr)
        return;
    if (pHead->m_pNext != nullptr)
    {
        print_linkList_recursively(pHead->m_pNext);
    }
    cout << pHead->m_nValue << endl;
}

ListNode* linkList_reverse(ListNode *pHead)
{
    if (pHead == nullptr)
    {
        return pHead;
    }
    ListNode *p1 = pHead, *p2 = pHead->m_pNext, *tmp = nullptr;
    pHead = nullptr;
    while (p2 != nullptr)
    {
        tmp = p2->m_pNext;
        p2->m_pNext = p1;
        p1 = p2;
        p2 = tmp;
    }
    pHead = p1;
    return pHead;
}
int main()
{
    std::cout << "Hello World!\n";
}

07前序和中序构建二叉树

每次递归前要检查左右子树序列是否存在

  1. 检查指针和长度
  2. 取出前序的头尾指针,中序的头尾指针
  3. 前序的第一个数,生成根节点,左右子树置空
  4. 检查前序是否头尾指针为同一个,在对中序进行输入异常检查(检查对的情况,其他的情况必然是错的)
    1. 是同一个,那么二叉树只有一个根节点,直接返回
    2. 不是,那么存在左子树和右子树
  5. 在中序找出中序根节点指针,并对中序进行输入异常检查
  6. 判断中序根节点指针前是否存在左子树
    1. 存在则递归实现左子树,将根节点的左指针指向递归函数返回值
    2. 不存在不处理,意味着递归出口
  7. 判断中序更阶段指针后是否存在右子树
    1. 存在则递归实现右子树,将根节点的右指针指向递归函数返回值
    2. 不存在不处理,意味着递归出口
  8. 返回根节点指针
/**
 * Definition for binary tree
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* reConstructBinaryTree(vector pre,vector vin) {
        return core(pre, vin, 0, pre.size() - 1, 0, vin.size() - 1);
    }
    TreeNode* core(vector pre, vector vin, int preStart, int preEnd, int inStart, int inEnd)
    {
        TreeNode* root = new TreeNode(pre[preStart]);
        if (preStart == preEnd)
            return root;
        int i = inStart;
        while(vin[i] != pre[preStart]) i++;
        int leftLen = i - inStart;
        int rightLen = inEnd - i;
        if (leftLen > 0)//非常重要
            root->left = core(pre, vin, preStart + 1, preStart + leftLen, inStart, i - 1);
        if (rightLen > 0)//非常重要
            root->right = core(pre, vin, preStart + leftLen + 1, preEnd, i + 1, inEnd);
        return root;
    }
};

08找出中序遍历的下一个节点

分三种情况,第三种是第二种的复杂模式

  1. 当前节点有右子节点,则右子节点是下一个节点
  2. 当前节点没有右子节点,但当前节点是父节点的左子节点,则父节点是下一个节点
  3. 当前节点没有右子节点,且当前节点是父节点的右子节点,则一直往上找一个节点,该节点是父节点的左子节点。那么该节点的父节点是下一个节点

异常:当前节点是最后的节点,则下一个节点为nullptr,实际上就是第三种情况未找到的结果的情况

#include 
typedef struct BinaryTreeNode
{
    int m_nVlaue;
    BinaryTreeNode* m_pLeft;
    BinaryTreeNode* m_pRight;
    BinaryTreeNode* m_pParent;
}BTN;

BTN* find_mid_next(BTN* node)
{
    if (node == nullptr)
    {
        return nullptr;
    }
    BTN* next = nullptr;
    if (node->m_pRight != nullptr)
    {
        BTN* child = node->m_pRight;
        while (child->m_pLeft != nullptr)
        {
            child = child->m_pLeft;
        }
        next = child;
    }
    else
    {
        BTN* parent = node->m_pParent;
        BTN* cur = node;
        while (parent != nullptr && parent->m_pLeft != cur)
        {
            cur = parent;
            parent = parent->m_pParent;
        }
        next = parent;
    }
    return next;
}
int main()
{
    std::cout << "Hello World!\n";
}

09两个栈实现队列

栈和队列是相互联系的

  1. 两个栈s1,s2
  2. 插入队尾时,将元素压入s1
  3. 删除对头时,检查s2是否为空(s1s2都为空则队列为空)
    1. 为空则将s1的全部元素出栈压入s2
    2. s2出栈一个元素
#include 
#include 
using namespace std;

template  class MyStack
{
public:
    MyStack() {}
    ~MyStack() {}
    T pop();
    void push(const T &element);
private:
    queue q1;
    queue q2;
};

template  void MyStack::push(const T &element)
{
    if (q1.empty())
        q2.push(element);
    else
        q1.push(element);
}

template  T MyStack::pop()
{
    if (q1.empty() && q2.empty())
        throw exception("queue is empty");
    T element;
    if (q1.empty())
    {
        while (q2.size() > 1)
        {
            q1.push(q2.front());
            q2.pop();
        }
        element = q2.front();
        q2.pop();
    }
    else
    {
        while (q1.size() > 1)
        {
            q2.push(q1.front());
            q1.pop();
        }
        element = q1.front();
        q1.pop();
    }
    return element;
}


int main()
{
    MyStack s1;
    s1.push(1);
    s1.push(2);
    s1.push(3);
    cout << s1.pop() << " ";
    s1.push(4);
    try
    {
        while(true)
            cout << s1.pop() << " ";
    }
    catch (const std::exception& e)
    {
        cout << e.what() << endl;
    }
    return 0;
}
//3421

09队列如何实现栈

  1. 设定两个队列q1,q2
  2. 压栈时,向q1,q2中不为空的队列压栈,如果都为空则默认压入q1队列
  3. 出栈前,如果有元素,必然是有一个队列为空,另一个不空
    1. 非空队列出列除队尾元素的所有元素到空队列
    2. 非空队列将队尾元素出栈

#include 
#include 
using namespace std;
template  class MyQueue
{
public:
    MyQueue(void);
    ~MyQueue(void);
    void appendTail(const T& node);
    T deleteHead(void);
private:
    stack s1;
    stack s2;
};

template  void MyQueue::appendTail(const T& node)
{
    s1.push(node);
}

template  T MyQueue::deleteHead(void)
{
    T head;
    if (!s2.empty())
    {
        head = s2.top();
        s2.pop();
    }
    else
    {
        while (!s1.empty())
        {
            s2.push(s1.top());
            s1.pop();
        }
        if (s2.empty())
            throw exception("MyQueue is empty");
    }
    head = s2.top();
    s2.pop();
    return head;
}
int main()
{
    std::cout << "Hello World!\n";
}

10斐波那契数列

小青蛙跳台阶略有不同,0, 1, 2开头

铺瓷砖同理

#include 
using namespace std;
long long Fibo_recursively(int n)
{
    if (n == 0)
        return 0;
    if (n == 1)
        return 1;
    return Fibo_recursively(n - 1) + Fibo_recursively(n - 2);
}

long long Fibo(int n)
{
    int res[2] = { 0, 1 };
    if (n < 2)
        return res[n];
    long long fiboN, fiboN_minus_1 = 1, fiboN_minus_2 = 0;
    for (int i = 2; i <= n; ++i)
    {
        fiboN = fiboN_minus_1 + fiboN_minus_2;
        fiboN_minus_2 = fiboN_minus_1;
        fiboN_minus_1 = fiboN;
    }
    return fiboN;
}
long long fi(int n) { int result[2] = { 0, 1 };	int i = 2;	long long num = 0;	if (n < 2) { return result[n]; }	long long fib_minusone = 1;	long long fib_minustwo = 0;	for (; i <= n; i++) { num = fib_minusone + fib_minustwo;		fib_minustwo = fib_minusone;		fib_minusone = num; }	return num; }

int main()
{
    //cout << Fibo_recursively(10)<< " " << Fibo(10) << endl;
    //cout << Fibo_recursively(100) << endl;
    //cout << Fibo(100) << endl;
    cout << Fibo(5000) << endl;
    cout << fi(5000) << endl;
    return 0;
}

11partition函数

在数组中选择一个数字,将数组的数字分为两个部分,比选择数小的数字移到数组左边,比选择数大的移动到右边

给出数组arr,开始下标start,结束下标end

  1. 随机从start到end取出一个作为基准数
  2. 将基准数与最后一个数对调
  3. 设定一个小于下标small=start-1
  4. 从start遍历数组到end-1
    1. 如果遍历到的数小于基准数
      1. ++small
      2. 如果遍历数不等于基准数,则对调遍历数和small下标指向的数
      3. 如果等于不处理,无需对调
  5. small的本质在于从起始处生成一个小于等于基准数的数列,small生成后,把基准数对调回来到small+1位置
  6. 将small加1下标返回,用作下一次的递归分界点
#include 
#include 
using namespace std;
int partition(int* arr, int start, int end)
{
    if (arr == nullptr || start < 0 || end < start)
    {
        return 0;
    }
    int index = start;
    swap(arr[index], arr[end]);
    int small = start - 1;
    for (index = start; index < end; ++index)
    {
        if (arr[index] < arr[end])
        {
            ++small;
            if (index != small)
                swap(arr[small], arr[index]);
        }
    }
    ++small;
    swap(arr[small], arr[end]);
    return small;
}

void quickSort(int *arr, int start, int end)
{
    if (start == end)
        return;
    int index = partition(arr, start, end);
    if (start < index)
        quickSort(arr, start, index - 1);
    if (index < end)
        quickSort(arr, index + 1, end);
}

int main()
{
    int a[] = { 7, 4, 3, 9,
        20, 1, 99, 8,
        7, 1, 6, 19 };
    swap(a[0], a[1]);
    cout << a[0] << a[1];

}

11快速排序

使用partition函数对数组进行递归解

递归出口是start==end

递归入口时start < index < end

11数字限定在较小范围内的0(n)方法排序

给公司所有人的年龄排序

  1. 人的年龄只有0-99岁
  2. 设定一个100长度数组
  3. 遍历所有人的年龄,在对应年龄数组上加1
  4. 按值次数输出数组下表
// offer11借助常量空间达到o_n方法数组排序.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;
void sortAge(int* ages, int length)
{
    if (ages == nullptr || length < 0)
        return;
    const int maxAge = 99;
    int ageTable[maxAge + 1] = { 0 };
    for (int i = 0; i < length; ++i)
    {
        if (i < 0 || i > maxAge)
            throw exception("invalid age.");
        ++ageTable[ages[i]];
    }
    int index = 0;
    for (int i = 0; i <= maxAge; ++i)
    {
        for (int j = 0; j < ageTable[i]; ++j)
        {
            ages[index++] = i;
        }
    }
}

int main()
{
    int ages[] = {
        1, 56, 87, 23, 12,
        43, 45, 23, 23, 56,
        12, 22, 99, 0, 11
    };
    sortAge(ages, 15);
    for (auto age : ages)
    {
        cout << age << " ";
    }
    return 0;
}

11旋转数组的最小数字

O(logN)的方法

递增数组,旋转之后的特性:前一段数组要大于等于后一段数组

  1. 指定idx1指向第一个数,idx2指向最后一个数
  2. 循环
    1. 循环条件是arr[idx1]>=arr[idx2]的数
    2. 退出条件是idx1和idx2相邻,其中idx2是最小值
    3. 取mid = (idx1+idx2)/2
    4. 特性应用
      1. 如果mid大于idx1,则idx1 = mid
      2. 如果mid小于idx2,则dix2= mid
  3. 补丁1
    1. 循环条件可能不满足,因为未旋转,那么第一个就是最小数(此时idx1的数 > idx2的数)
  4. 补丁2
    1. 特性利用不上,无法判断mid给idx1还是idx2,需要遍历找出(此时idx1 idx2 mid 的数都相等)

特例1:增值数组其实没有旋转,最小数在第一个,意味着不满足旋转数组特性

特例2:index1和index2和mid的值全部相同,无法判断是在哪一段数组上,此时要遍历[index1, index2]

#include 
using namespace std;

int find(int* arr, int start, int end)
{
    int min = arr[start];
    for (int i = start + 1; i <= end; ++i)
    {
        if (min > arr[i])
            min = arr[i];
    }
    return min;
}

int findmin(int* arr, int length)
{
    if (arr == nullptr || length < 0)
        throw exception("invalid input");
    int idx1 = 0;
    int idx2 = length - 1;
    int mid = idx1;//未旋转特例
    while (arr[idx1] >= arr[idx2])
    {
        if (idx2 - idx1 == 1) 
        {
            mid = idx2;
            break;
        }
        mid = (idx1 + idx2) / 2;

        if (arr[idx1] == arr[mid] && arr[mid] == arr[idx2])
        {
            return find(arr, idx1, idx2);
        }

        if (arr[mid] >= arr[idx1])
            idx1 = mid;
        if (arr[mid] <= arr[idx2])
            idx2 = mid;
    }
    return arr[mid];
}
int  NumberOf1(int n) {
    int cnt = 0;
    if (n < 0)
        ++cnt;
    int mask = 1;
    for (int i = 0; i < sizeof(int) * 8 - 1; ++i)
    {
        mask = mask << i;
        if (mask & n != 0)
            ++cnt;
    }
    return cnt;
}

#include 
void reOrderArray(vector& array) {
    int length = array.size();
    if (length <= 1)
        return;
    int head = 0, tail = length - 1;
    while (head < tail)
    {
        while ((array[head] & 1) != 0)//找偶数
            head++;
        while ((array[tail] & 1) == 0)//找奇数
            tail--;
        if (head < tail)
        {
            swap(array[head], array[tail]);
        }
    }
}

int main()
{
    vector a = { 0,3,2,5,4,7,8,9 };
    reOrderArray(a);
    for (auto i : a)
        cout << i << " ";
    return 0;
}

12矩阵中的路径

回溯法:

对每个矩阵元素进行遍历,如果当前符合,则探索子节点可能性。子节点具有可能性则继续往具有可能性的子节点探索,如果不具可能性则返回的遍历父节点,到父节点的相邻节点进行探索。

注意对具有可能性的节点进行标记,后面发现没有可能性则取消标记(即如果要对当前节点的子节点探索则先标记当前节点)

  1. 建立标记矩阵,标记矩阵记录被选中的和字符串的字符匹配的坐标

  2. 对矩阵所有节点进行调用递归函数探索每个节点可能性

  3. 探索递归函数工作

    1. 需要保持对字符串的下标追踪
    2. 保持对标记矩阵的追踪
    3. 可以探索任意一个坐标(行列坐标加一减一即可,但是函数要判断行列坐标合法性,确保不会越界)
    4. 具有成功出口,追踪到了‘\0’,即成功(叶节点触底用的)
    5. 具有成功/失败出口,设立失败标志,如果探索下面的子节点失败则直接返回失败标志,如果成功返回成功
    6. 递归的核心在于不断查找叶节点可能性,叶节点触底即可知道是否成功,然后返回
  4. 一直贯穿始终的是下表追踪和标记追踪,一直是单线查找的

    1. 所以当前下标触及叶节点发现不可行,则会回退一下;标记节点也是
    2. 然后返回上一级递归,上一级递归会知晓当前追踪状态
#include 
using namespace std;

bool hasPathCore(const char *matrix, int rows, int columns,
    int row, int column, const char* str, int& str_idx, bool *mark)
{
    if (str[str_idx] == '\0')
        return true;
    bool result = false;
    if (row >= 0 && row < rows && column >= 0 && column < columns &&
        mark[row * columns + column] == false &&
        matrix[row * columns + column] == str[str_idx])
    {
        ++str_idx;
        mark[row * columns + column] = true;
        result = hasPathCore(matrix, rows, columns, row + 1, column,
            str, str_idx, mark) ||
            hasPathCore(matrix, rows, columns, row - 1, column,
                str, str_idx, mark) ||
            hasPathCore(matrix, rows, columns, row, column + 1,
                str, str_idx, mark) ||
            hasPathCore(matrix, rows, columns, row, column - 1,
                str, str_idx, mark);
        if (!result)
        {
            --str_idx;
            mark[row * columns + column] = false;
        }
    }
    return result;
}

bool hasPath(const char* matrix, int rows, int columns, const char* str)
{
    if (matrix == nullptr || rows < 0 || columns < 0 || str == nullptr)
        return false;
    bool* mark = new bool[rows * columns];
    memset(mark, 0, rows * columns);
    int str_idx = 0;
    for (int row = 0; row < rows; ++row)
    {
        for (int column = 0; column < columns; ++column)
        {
            if (hasPathCore(matrix, rows, columns, row, column,
                str, str_idx, mark))
            {
                return true;
            }
        }
    }
    delete[] mark;
    return false;
}

int main()
{
    char matrix[] = {
        'a', 'b', 't', 'g',
        'c', 'f', 'c', 's',
        'j', 'd', 'e', 'h'
    };
    const char* str = "bfce";
    const char* str1 = "bfch";
    const char* str2 = "afce";
    cout << hasPath(matrix, 3, 4, str) << hasPath(matrix, 3, 4, str1) << hasPath(matrix, 3, 4, str2);
    return 0;
}

13机器人的运动范围

在二维矩阵运动的问题都可以用回溯法解决

  1. 机器人从坐标00移动,进入ij坐标时检查限制条件是否可以进入,
  2. 进入之后,计数加一,
    1. 递归看四个方向可否进入
    2. 另外已经进入过的就无需再进入,因为已经统计过
#include 

int digitSum(int row, int col)
{
    int sum = 0;
    while (row || col)
    {
        sum = row % 10 + col % 10;
        row /= 10;
        col /= 10;
    }
    return sum;
}

bool check(int rows, int cols, int row, int col, int k, bool* mark)
{
    if (row >= 0 && row < rows &&
        col >= 0 && col < cols &&
        digitSum(row, col) <= k &&
        !mark[row * cols + col])
        return true;
    return false;
}

int moveCountCore(int rows, int cols, int row, int col, bool* mark)
{
    int cnt = 0;
    if (check(rows, cols, row, col, 18, mark))
    {
        mark[row * cols + col] = true;
        cnt = 1 + moveCountCore(rows, cols, row + 1, col, mark) +
            moveCountCore(rows, cols, row - 1, col, mark) +
            moveCountCore(rows, cols, row, col + 1, mark) +
            moveCountCore(rows, cols, row, col - 1, mark);
    }
    return cnt;
}

int moveCount(int rows, int cols, int k)
{
    if (rows < 0 || cols < 0 || k < 0)
        return 0;
    bool* mark = new bool[rows * cols];
    memset(mark, 0, rows * cols);
    return moveCountCore(rows, cols, 0, 0, mark);
}


int main()
{
     
    std::cout << sizeof(H);
}

14动态规划

特点:

  1. 求问题的最优解(最大值或最小值)
  2. 问整体问题最优解依赖于子问题的最优解(大问题有通过小问题求解的公式)
  3. 子问题之间有相互重叠的更小子问题
  4. 从上往下分析,从下往上求解(把小问题最优解用数组存起来)

14贪婪算法

特点:

  1. 每一步做出贪婪的选择,基于该选择可以得到最优解

14剪绳子

动态规划判断:

  1. 求最大值
  2. n长绳子的最大值f(n),被剪i长后的最大值为f(n-i) * f(i)。其中f(n-i) 和 f(i)也是最优解。
  3. 有重叠子问题
  4. 从第一刀分析,从最小特殊问题求解

动态递归方法:

  1. 写出简单的最小子问题最优解,写出procedure数组前面几个数

    1. **注意!!!!**最小子问题最优解和procedure数组前面几个数不一定相同
  2. 最小子问题不用满足必须剪1刀,因为最小子问题组成的大问题一定被剪了一刀以上。如果单独求最小子问题则需要满足必须剪一刀的条件。
    3. 列出几个项的特殊值,取决于特殊子问题和procedure的不同有几个

  3. 设立数组,记录f(n)的最优解

  4. f(n)的最优解可以通过f(0)~f(n-1)求解,求解公式f(n) = max(f(n-i)*f(i)), 其中 0

  5. 第一个循环,遍历i;第二个循环用于对i用递归求解公式逻辑求出最大值

#include 
using namespace std;

int cut(int n)
{
    if (n < 2)
        return -1;
    if (n == 2)
        return 1;
    if (n == 3)
        return 2;
    int* f = new int[n + 1];
    f[0] = 0;
    f[1] = 1;
    f[2] = 2;
    f[3] = 3;
    int max = 0;
    for (int i = 4; i <= n; ++i)
    {
        max = 0;
        for (int j = 1; j < i; ++j)
        {
            if (max < f[j] * f[i - j])
                max = f[j] * f[i - j];
        }
        f[i] = max;
    }
    max = f[n];
    delete[] f;
    return max;
}

int main()
{
    std::cout << "Hello World!\n";
}

15.位运算

逻辑右移(高位补0)算术右移(高位补符号位)

逻辑左移(低位补0)算术左移(低位补0)

按照移位补0的原则,为何左移都是逻辑移位呢?

**答疑:**先看看“-8”和“8”在计算机内存中的值分别是:

0xfffffff8

0x8

由于计算机均按补码保存数值,所以不管符号正负,左移对于符号位并不产生影响

15判断整数二进制中1的个数

解法1

不断用1左移然后和整数做&处理,处理结果不等于0则代表该位为1

解法2重要!!!!!

把整数减去1,和原整数做&运算,会把最右边的1变成0,如果有n个1,可以做n次这样的运算

n = n & (n - 1);

解法2是二进制问题的银弹

#include 

int numberOf1(int n)
{
    int flag = 1;
    int cnt = 0;
    while (flag)
    {
        if (n & flag)
            ++cnt;
        flag = flag << 1;
    }
    return cnt;
}

int numberOf1_(int n)
{
    int cnt = 0;
    while (n)
    {
        n = (n - 1) & n;
        ++cnt;
    }
    return cnt;
}

int changeBit(int m, int n)
{
    int cnt = 0;
    int mn = m ^ n;
    while (mn)
    {
        mn = mn & (mn - 1);
        ++cnt;
    }
    return cnt;
}

int main()
{
    std::cout << numberOf1(9) << numberOf1_(9) << changeBit(10, 13);
    return 0;
}

15一条语句判断一个整数是否是2的整数次方

如果是,那么二进制只有一位是1,其余为零。使用解法2可以做到

if ( n > 1 && !(n & (n - 1)))

15输入整数 m 和 n,需要改变m的二进制中多少位才能得到n

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FMJxF1d2-1586255098442)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1584710298237.png)]

位运算的& | ^ ~的运算,不排除符号位,即符号位也要参加运算

  1. 求这两这数的异或
  2. 统计这两个数的异或结果1的个数

15 本章小结

  1. 二分查找、快排、归并排序是极度高频
  2. 回溯法适合解迷宫,二位平面的运动
  3. 要求最优解可以尝试动态规划
  4. 如果动态规划的分析发现每一步都存在最优解,则使用贪婪算法
  5. 问题一般是自上而下的递归分析,代码求解则是基于自下而上的循环实现
  6. 二进制问题一般都是用位运算解决

16 数值的整数次方,不考虑大数问题

解法1

  1. 次方可能为负数、0、正数
  2. 次方为负数且底数为0,是特殊情况
  3. 次方为负数时需要求解倒数

注意!!!!

double判断相等不能使用 == ,而是使用equal函数。因为,代码中逻辑上相等的两个数,在实际存储中会有微小误差

//判断整数的equal函数
#include 
#define MINVALUE (1e-8)
bool myEqual(double m, double n)
{
    return abs(m - n) < MINVALUE;
}
double参与运算时
要 1.0 / double

解法2[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fQBwgF58-1586255098444)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1584714025515.png)]

通过递归实现这个公式,达到logN求解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DJOIveW1-1586255098445)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1584714607641.png)]

17打印从1到最大的n位数

陷阱在于大数问题,大数问题需要使用字符串或者数组解决

方法1:

  1. 在字符串上模拟数字加法
  2. 把字符串表达的数字打印出来

细节:

  1. 字符串加法函数如何判断到达了N位数的最大值
    1. 对第一个字符检测是否产生了进位,达到O(1)时间判断
    2. 而不是使用strcmp(str, ”99999999“)
  2. 打印字符串,如何把前面的0字符去掉

方法2:

  1. 使用递归把所有数字排列出来
  2. 把字符串表达的数字打印出来

总结:

​ 如果面试关于N位数的整数,且没有限定n的范围,则可能需要大数问题

#include 
using namespace std;

void printNumber(const char* number)
{
    int notZeroIndex = 0;
    while (number[notZeroIndex] == '0')
        ++notZeroIndex;
    printf("%s\n", number + notZeroIndex);
    return;
}

bool numberIncriment(char *number, int n)
{
    bool isOverflow = false;
    int flag = 0;
    int index = n - 1;
    while (index >= 0)
    {
        int num = number[index] - '0' + flag;//flag执行进位
        if (index == n - 1)
            ++num;//执行函数加1操作
        if (num < 10)
        {
            flag = 0;
            number[index] = '0' + num;
            break;
        }
        flag = 1;
        num -= 10;
        number[index] = '0' + num;
        --index;
    }
    if (index < 0 && flag == 1)
        isOverflow = true;
    return isOverflow;
}

void print1ToMaxOfNDigits(int n)
{
    if (n < 1)
        return;
    char* number = new char[n + 1];
    memset(number, '0', n + 1);
    number[n] = '\0';
    while (!numberIncriment(number, n))
    {
        printNumber(number);
    }
    delete[] number;
}

void print1ToMaxOfNDigitsRecursivelyCore(char* number, int n, int index)
{
    if (index == n)
    {
        printNumber(number);
        return;
    }
    for (int i = 0; i < 10; ++i)
    {
        number[index] = '0' + i;
        print1ToMaxOfNDigitsRecursivelyCore(number, n, index + 1);
    }
    return;
}

void print1ToMaxOfNDigitsRecursively(int n)
{
    if (n < 1)
        return;
    char* number = new char[n + 1];
    memset(number, '0', n + 1);
    number[n] = '\0';
    print1ToMaxOfNDigitsRecursivelyCore(number, n, 0);
    delete[] number;
    return;
}


int main()
{
    //print1ToMaxOfNDigits(9);
    print1ToMaxOfNDigitsRecursively(8);
    return 0;
}

18删除链表的节点

delete指针后,指针要考虑清零

题目1:O(1)时间删除节点

  1. 将节点指针的下一个节点的值复制到节点上
  2. 注意删除尾节点要遍历
  3. 如果删除头尾节点要把head置空

题目2:排序链表中,删除链表重复节点。例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

  1. 头节点有可能被删除,所以传入头指针要用二层指针
  2. 需要引入pre指针用于删除,pre一开始为null
  3. 需要引入cur指针用于检查重复,cur->val == cur->next->val
  4. 可能会有连续好几个重复节点,删除需要使用while循环删除

难点:如何处理好pre指针一开始为空,追随在cur之后的问题

ListNode *pPre = nullptr;
ListNode *pCur = *pHead;
ListNode *pNext = nullptr;

while (pCUr != nullptr)
{
    pNext = pCur->next;//衔接next
    if(pCur don't need process)
       {
           pPre = pCur;//衔接pre
           pCur = pNext;//衔接cur
       }
       else
       {
           //这里pCur 遍历的节点都被删除
           //pNext进行了下移
           while(pCur != nullptr || pCur need delete)
           {
               pNext = pCur->next;
               delete pCur;
               pCur = pNext;
           }
           
               
           if (pPre == nullptr)//头节点被删除了
           {
               *pHead = pNext;//特殊的头节点
               //pre依旧停留在nullptr
               pCur = pNext;//衔接cur
           }
           else
           {
               pPre->next = pNext;//衔接pre
           	   pCur = pNext;//衔接cur
           }
       }
}

offer19 正则表达式匹配

.可以匹配任意一个字符

*可以匹配0或任意一个前面的字符

核心:递归思路,逐步匹配每一个字符串

  1. 如果匹配中两个字符串为 ‘\0’,返回true //递归出口
  2. str不为‘\0’,而patter为’\0’,返回false //递归出口
    1. str为’\0’时,可以被x*匹配
  3. 当pattern+1为*
    1. str和pattern匹配(str == patter || (str != ‘\0’ && patter = ‘.’)时,递归(str+1, patern)
    2. str和pattern不匹配,(str == ‘\0’)时递归匹配(str,pattern+2),(str != ‘’\0)时递归匹配(str+1, pattern+2)
  4. 当patter+1不为*
    1. str和pattern匹配(str == patter || (str != ‘\0’ && patter = ‘.’),递归(str+1, patern+1)
    2. 不匹配,返回false //递归出口

注意:这里str为’\0’时,也是可以和x*匹配的,这是(str,pattern+2)的来源

#include 

bool matchCore(const char* str, const char* pattern)
{
    if (*str == '\0' && *pattern == '\0')
        return true;
    if (*str != '\0' && *pattern == '\0')
        return false;
    //*str == '\0' && *pattern != '\0' 有可能会被x*匹配
    if (*(pattern + 1) == '*')
    {
        //如果当前str匹配pattern
        if (*str == *pattern || (*str != '\0' && *pattern == '.'))
        {
            return matchCore(str + 1, pattern);
        }
        //如果当前str不匹配pattern
        else
        {
            //'\0'被连续的X*匹配
            if (*str == '\0')
                return matchCore(str, pattern + 2);
            else
                return matchCore(str + 1, pattern + 2);
        }
    }
    else
    {
        if (*pattern == *str || (*pattern == '.' && *str != '\0'))
        {
            return matchCore(str + 1, pattern + 1);
        }
        else
        {
            return false;
        }
    }
}

bool match(const char* str, const char* pattern)
{
    if (!str || !pattern)
        return false;
    return matchCore(str, pattern);
}
int main()
{
    std::cout << "Hello World!\n";
}

20 表示数值的字符串

A[.[B]][e|EC]

.B[e|EC]

A和C前面可以有+ - ,B前面不可以有+ -,ABC都是0-9

小数点后面可以没有小数,前面可以没有整数,.123 和123.都是成立的

// offer20 表示数值的字符串.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

//扫描正整数,并且把指针移动到正整数后面,如果没有扫描到则返回false
bool scanUnsignedInt(const char** str)
{
    const char* before = *str;
    while (**str != '\0' && **str > '0' && **str < '9')
        ++(*str);
    return *str > before;
}

//扫描整数,并且把指针移动到正整数后面,如果没有扫描到则返回false
bool scanInt(const char** str)
{
    if (**str == '+' || **str == '-')
        ++(*str);
    return scanUnsignedInt(str);
}

bool isNumeric(const char* str)
{
    if (!str)
        return false;
    //把整数部分全部扫描
    bool isNum = scanInt(&str);
    //如果前面的没有扫描到整数,那么不可能匹配到‘.’
    if (*str == '.')
    {
        ++str;
        //小数点前或后可以没有数字
        isNum = scanInt(&str) || isNum;
    }
    //前面的整数和‘.’都被扫面干净,现在看有没有e
    if (*str == 'e' || *str == 'E')
    {
        ++str
        //e的前后必须要有数字
        isNum = isNum && scanInt(&str);
    }
    //e之后的整数扫面之后,字符串必须结尾
    return isNum && *str == '\0';
}

int main()
{
    std::cout <

21调整数组顺序使奇数在偶数前面

核心思想:头指针和尾指针,头指针向后移动直到移动到偶数,尾指针向前移动直到奇数。如果尾指针还在头指针后面,则交换两个指针的值

解耦:判断奇偶应该用函数指针判断,可以处理一大类问题

失误:注释部分即是失误部分

失误1
if(length < 0)//这里应该要length <= 0,因为end = arr+length-1;
    return;

失误2
while (end > begin)
    {
        //end > begin是必须的,否则会导致指针溢出
        while (end > begin && func(*begin))
            ++begin;
    }
#include 
using namespace std;
bool isOdd(int n)
{
    return (n & 0x1) == 1;
}

void changeArray(int* arr, int length, bool (*func)(int))
{
    if (arr == nullptr || length <= 0)
        return;
    int* begin = arr;
    int* end = arr + length - 1;
    while (end > begin)
    {
        //end > begin && 是必须的,否则会导致指针溢出
        while (end > begin && func(*begin))//找到偶数数
            ++begin;
        while (end > begin && !func(*end))//找到奇数
            --end;
        if (end > begin)
        {
            int tmp = *begin;
            *begin = *end;
            *end = tmp;
        }
    }
}


int main()
{
    int arr1[] = { 1,5, 1, 1, 1, 1, 1, 1, 1, 3 };
    changeArray(arr1, 10, isOdd);
    for (auto i : arr1)
        cout << i << " ";
    return 0;
}

22求单向链表的倒数第K个节点(最后一个是倒数第一个)

核心遍历思想:当遍历单向链表需要不止一次的时候,可以设定两个指针一前一后一起遍历,做到一次遍历解决问题。

解题思路:

​ 设定指针ahead和behind,ahead先移动k-1位,然后ahead和behind一起移动,直到最后一个节点,这样behind指向的就是倒数第K个节点

陷阱:

  1. 链表为空
  2. 链表节点个数小于k个
  3. k输入为0,而ahead需要先移动k-1位
// offer22获取单向链表倒数第k个节点.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;

struct ListNode
{
    int m_nVal;
    ListNode* m_pNext;
};

ListNode* findReverseK(ListNode* head, unsigned int k)
{
    if (head == nullptr || k == 0)
        return nullptr;
    ListNode* ahead = head;
    ListNode* behind = head;
    for (unsigned int i = 0; i < k - 1; ++k)
    {
        if (ahead->m_pNext)
            ahead = ahead->m_pNext;
        else
            return nullptr;
    }
    while (ahead->m_pNext)
    {
        ahead = ahead->m_pNext;
        behind = behind->m_pNext;
    }
    return behind;
}

int main()
{
    std::cout << "Hello World!\n";
}

22求链表的中间节点

设定两个指针p1 和p2

​ //head 不为空
​ //只有1个或2节点直接返回头节点
​ //p1每次走1步,p2每次走两步
​ //p2完成不了两步的时候,直接返回p1
​ //p2两步之后到达最后节点,直接返回p1

// offer22获取链表中间节点.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

struct ListNode
{
    int m_nVal;
    ListNode* m_pNext;
};

ListNode* findMidNode(ListNode* head)
{
    //head 不为空
    //只有1个或2节点直接返回头节点
    //p1每次走1步,p2每次走两步
    //p2完成不了两步的时候,直接返回p1
    //p2两步之后到达最后节点,直接返回p1
    if (!head)
        return nullptr;
    if (head->m_pNext == nullptr || head->m_pNext->m_pNext == nullptr)
        return head;
    ListNode* p1 = head;
    ListNode* p2 = head;
    while (p2->m_pNext)
    {
        if (p2->m_pNext->m_pNext == nullptr)
            return p1;
        p2 = p2->m_pNext->m_pNext;
        p1 = p1->m_pNext;
    }
    return p1;
}

int main()
{
    std::cout << "Hello World!\n";
}

23求链表环的入口节点

核心思想

  1. 先确定有没有环,通过两个指针p1和p2,p2走两步,p1走一步,如果p2追上p1说明有环
    1. 任意一个环,一个走两步一个走一步,始终会重合上一次
  2. 确定环内节点数,1中p2追上p1之后,p2不动,p1继续走,p1再次会回到p2就是节点数N。
  3. 查找节点入口,p1、p2指向head,p2先走N步,然后p1、p2同时每次走一步,两者第一次相遇的点就是入口点

24反转链表

  1. 链表为空,返回nullptr
  2. 链表只有1个节点,返回head
  3. 链表有两个节点及以上,设置pre,node,next三个节点,next用于保存node的下一个节点防止断链
    1. 一开始node置为head,pre和next置为nullptr
    2. while循环,条件是node不为空
// offer24反转链表.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

struct ListNode
{
    int m_nVal;
    ListNode* m_pNext;
};

ListNode* listReverse(ListNode* head)
{
    if (!head)
        return nullptr;
    if (head->m_pNext == nullptr)
        return head;
    ListNode* pre = nullptr;
    ListNode* node = head;
    ListNode* next = nullptr;
    while (node != nullptr)
    {
        next = node->m_pNext;
        node->m_pNext = pre;
        pre = node;
        node = next;
    }
    return pre;
}

int main()
{
    std::cout << "Hello World!\n";
}

25合并两个排序的链表

  1. robust,确保空指针问题
  2. 选择头节点最小的那条链表作为其实合并点
  3. 依次从两条链表剩余节点中找出最小的节点,链接到合并节点。可以使用递归解决
// offer25合并两个排序的链表.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
struct ListNode
{
    int m_nVal;
    ListNode* m_pNext;
};

ListNode* merge(ListNode* head1, ListNode* head2)
{
    if (!head1)
        return head2;
    if (!head2)
        return head1;
    ListNode* mergeHead= nullptr;
    if (head1->m_nVal > head2->m_nVal)
    {
        mergeHead = head2;
        mergeHead->m_pNext = merge(head1, head2->m_pNext);
    }
    else
    {
        mergeHead = head1;
        mergeHead->m_pNext = merge(head1->m_pNext, head2);
    }
    return mergeHead;
}


int main()
{
    std::cout << "Hello World!\n";
}

26树的子结构

分两步走

  1. 找出A树中和B树根节点值一样的节点
    1. A树先序遍历查找
    2. A树或B树为null返回false
    3. A树查找到节点进入第二步判断,先找左子树,左子树不行找右子树
  2. 判断A树的这个节点的子树是否包含B树结构
    1. 先序递归对比B树结构
    2. 如果A树为null,false;如果B树为null,true;
    3. 如果左右子树都相等返回true

1和2都可以采用树的先序遍历,递归思路

如果有两层递归,则定义两个递归函数

bool hasChildTreeCore(BinaryTreeNode* pRoot1, BinaryTreeNode* pRoot2)
{
    //检查空指针问题
    //必须先检查root2
    //如果roo2为空,说明root2的节点全部匹配成功,返回true;
    //如果root1为空,说明root1已经没有节点来匹配了,返回false
    //判断两个根节点是否相同
    //先序同时遍历两颗树的左右子树的节点是否相等
    if (!pRoot2)
        return true;
    if (!pRoot2)
        return false;
    bool result = false;
    if (!doubleEqual(pRoot1->m_dVal, pRoot2->m_dVal))
        return false;
    return hasChildTreeCore(pRoot1->m_pLeft, pRoot2->m_pLeft) &&
        hasChildTreeCore(pRoot1->m_pRight, pRoot2->m_pRight);
}

bool hasChildTree(BinaryTreeNode* pRoot1, BinaryTreeNode*pRoot2)
{
    //先检查空指针问题
    //对比两个根节点值是否相等
    //相等,进入core函数判断子树是否相等
    //不等,递归查看root1的左右子树和root2是否相等
    if (pRoot1 == nullptr || pRoot2 == nullptr)//递归出口
        return false;
    if (doubleEqual(pRoot1->m_dVal, pRoot2->m_dVal))
    {
        return hasChildTreeCore(pRoot1, pRoot2);//递归出口
    }
    else
    {
        return hasChildTree(pRoot1->m_pLeft, pRoot2) ||//递归出口
            hasChildTree(pRoot1->m_pRight, pRoot2);
    }
}
// offer26树的子结构.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

struct BinaryTreeNode
{
    double m_dVal;
    BinaryTreeNode* m_pLeft;
    BinaryTreeNode* m_pRight;
};

bool doubleEqual(double b1, double b2)
{
    return (b1 - b2) < 1e-8 || (b2 - b1) < 1e-8;
}

bool hasChildTreeCore(BinaryTreeNode* pRoot1, BinaryTreeNode* pRoot2)
{
    //检查空指针问题
    //必须先检查root2
    //如果roo2为空,说明root2的节点全部匹配成功,返回true;
    //如果root1为空,说明root1已经没有节点来匹配了,返回false
    //判断两个根节点是否相同
    //先序同时遍历两颗树的左右子树的节点是否相等
    if (!pRoot2)
        return true;
    if (!pRoot2)
        return false;
    bool result = false;
    if (!doubleEqual(pRoot1->m_dVal, pRoot2->m_dVal))
        return false;
    return hasChildTreeCore(pRoot1->m_pLeft, pRoot2->m_pLeft) &&
        hasChildTreeCore(pRoot1->m_pRight, pRoot2->m_pRight);
}

bool hasChildTree(BinaryTreeNode* pRoot1, BinaryTreeNode*pRoot2)
{
    //先检查空指针问题
    //对比两个根节点值是否相等
    //相等,进入core函数判断子树是否相等
    //不等,递归查看root1的左右子树和root2是否相等
    if (pRoot1 == nullptr || pRoot2 == nullptr)//递归出口
        return false;
    if (doubleEqual(pRoot1->m_dVal, pRoot2->m_dVal))
    {
        return hasChildTreeCore(pRoot1, pRoot2);//递归出口
    }
    else
    {
        return hasChildTree(pRoot1->m_pLeft, pRoot2) ||//递归出口
            hasChildTree(pRoot1->m_pRight, pRoot2);
    }
}

int main()
{
    std::cout << "Hello World!\n";
}

27二叉树的镜像

先序遍历每一个节点,如果节点的左右子树至少存在一个,则交换该节点的指针;如果到达叶节点,直接返回。注意空节点的处理

// offer27 树的镜像.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

void mirrorTree(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return;
    if (!pRoot->m_pLeft && !pRoot->m_pRight)
        return;
    BinaryNodeTree* temp = nullptr;
    temp = pRoot->m_pLeft;
    pRoot->m_pLeft = pRoot->m_pRight;
    pRoot->m_pRight = temp;
    mirrorTree(pRoot->m_pLeft);
    mirrorTree(pRoot->m_pRight);
}

int main()
{
    std::cout << "Hello World!\n";
}

28对称的二叉树

对比二叉树的前序遍历序列和对称前序遍历序列来判断二叉树是否对称,如果两个序列是一样的,那么是对称。

要将nullptr指针考虑在内

#include 

struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

bool isSymmetricalCore(BinaryNodeTree* pRoot1, BinaryNodeTree* pRoot2)
{
    if (!pRoot1 && !pRoot2)
        return true;
    if (!pRoot1 || !pRoot2)
        return false;
    if (pRoot1->m_nVal != pRoot2->m_nVal)
        return false;
    return isSymmetricalCore(pRoot1->m_pLeft, pRoot2->m_pRight) &&
        isSymmetricalCore(pRoot1->m_pRight, pRoot2->m_pLeft);
}

bool isSymmetrical(BinaryNodeTree* pRoot)
{
    return isSymmetricalCore(pRoot, pRoot);
}

int main()
{
    std::cout << "Hello World!\n";
}
bool isSymmetricalCore(BinaryNodeTree* pRoot1, BinaryNodeTree* pRoot2)
{
    if (!pRoot1 && !pRoot2)//叶节点的nullptr比较
        return true;
    if (!pRoot1 || !pRoot2)//nullptr比较
        return false;
    if (pRoot1->m_nVal != pRoot2->m_nVal)//先序遍历比较,两个树的根节点
        return false;
    return isSymmetricalCore(pRoot1->m_pLeft, pRoot2->m_pRight) &&
        isSymmetricalCore(pRoot1->m_pRight, pRoot2->m_pLeft);//先序遍历比较左右节点
}

bool isSymmetrical(BinaryNodeTree* pRoot)
{
    return isSymmetricalCore(pRoot, pRoot);
}

29顺时针打印矩阵

1584847126822 1584880805001

分解

第一个循环确认一圈一圈是否可以打印

第二个循环打印每一圈

// offer29顺时针打印矩阵.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
using namespace std;

void printCircleCore(int* matrix, int rows, int cols, int start)
{
    int endRow = rows - 1 - start;
    int endCol = cols - 1 - start;
    //固定第start行打印
    for (int col = start; col <= endCol; ++col)
        cout << matrix[start * cols + col] << " ";
    //固定第endCol列打印
    for (int row = start + 1; row <= endRow; ++row)
        cout << matrix[row * cols + endCol] << " ";
    //固定第endRow行打印 
    for (int col = endCol - 1; col >= start; --col)
        cout << matrix[endRow * cols + col] << " ";
    //固定第start列打印
    for (int row = endRow - 1; row > start; --row)
        cout << matrix[row * cols + start] << " ";
}

void printCircle(int* matrix, int rows, int cols)
{
    if (matrix == nullptr || rows <= 0 || cols <= 0)
        return;
    int start = 0;
    while (rows > 2 * start && cols > 2 * start)
    {
        cout << start << ":\n";
        printCircleCore(matrix, rows, cols, start);
        cout << endl;
        ++start;
    }
}

vector printMatrix(vector > matrix) {
    int rows = matrix.size();
    int cols = matrix[0].size();
    vector printSerials;
    for (int start = 0; (2 * start < rows) && (2 * start < cols); ++start)
    {
        int endRow = rows - 1 - start;
        int endCol = cols - 1 - start;
        //固定行,移动列
        for (int col1 = start; col1 <= endCol; ++col1)
            printSerials.push_back(matrix[start][col1]);
        //固定列,移动行
        for (int row1 = start + 1; row1 <= endRow; ++row1)
            printSerials.push_back(matrix[row1][endCol]);
        //固定行,移动列
        for (int col2 = endCol - 1; col2 >= start; --col2)
            printSerials.push_back(matrix[endRow][col2]);
        //固定列,移动行
        for (int row2 = endRow - 1; row2 >= start + 1; --row2)
            printSerials.push_back(matrix[row2][start]);
    }
    return printSerials;
}

int main()
{
    vector> mat;
    vector m1 = { 1, 2, 3 };
    vector m2 = { 4, 5, 6 };
    vector m3 = { 7, 8, 9 };
    mat.push_back(m1);
    mat.push_back(m2);
    mat.push_back(m3);


    vector r = printMatrix(mat);
    for (auto i : r)
        cout << i << " ";
    return 0;
}

30包含min函数的栈

一个可以实现min、push、pop的栈

思路:

设置一个辅助栈,每次栈需要push的时候,对辅助栈push一个min值(辅助栈栈顶元素或者被push的元素)。

每次需要pop的时候,辅助栈也跟着pop,保证辅助栈顶的最小值是栈的最小值

需要执行min的时候就对辅助栈执行top

// offer30含min函数的栈.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
using namespace std;
template  class StackWithMin
{
public:
    void myPop();
    void myPush(T element);
    T myTop();
    T min();
private:
    stack s;
    stack s_assistant;
};

template  void StackWithMin::myPop()
{
    //assert(s.size() > 0 && s_assistant.size() > 0);
    s.pop();
    s_assistant.pop();
}

template  void StackWithMin::myPush(T element)
{
    s.push(element);
    T min = element;
    if (s_assistant.size() > 0 && s_assistant.top() < element)
        min = s_assistant.top();
    s_assistant.push(min);
}

template  T StackWithMin::myTop()
{
    return s.top();
}

template  T StackWithMin::min()
{
    return s_assistant.top();
}

int main()
{
    StackWithMin s;
    s.myPush(1);
    s.myPush(4);
    s.myPush(0); 
    s.myPush(-1);
    cout << "min = " << s.min();
    s.myPop();
    cout << "min = " << s.min();
    return 0;
}

31栈的压入弹出序列

给定一个压栈序列A,判断某一序列B是否是出栈序列,假定序列中数字全部不一样

思路

设定一个辅助栈S,

下一个需要弹出的数字正好在S栈顶,那么直接弹出

下一个需要弹出的数字不在S栈顶,把压栈序列A中还没有入栈的序列入栈,直到压入需要弹出的数字为止

如果序列A为空,还没有找到那个出栈数字,则说明B不是出栈序列

如果序列B成功遍历完了且 S为空(S为空说明B的最后一个元素也符合),说明是弹出序列

// offer31栈的压入弹出序列.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
using namespace std;

bool isPopOrder(const int* pushOrder, const int* popOrder, int length)
{
    if (length < 0 || !pushOrder || !popOrder)
        return false;
    bool result = false;
    stack s;
    const int* pPush = pushOrder;
    const int* pPop = popOrder;
    while ((pPop - popOrder) < length)
    {
        while (s.empty() || s.top != *pPop)
        {
            if ((pPush - pushOrder) < length)
            {
                s.push(*pPush);
                ++pPush;
            }
            else
            {
                break;
            }
        }
        if (s.top != *pPop)
            break;
        else
        {
            s.pop();
            ++pPop;
        }
    }
    if ((pPop - popOrder) == length && s.empty())
        result = true;

    return result;
}

int main()
{
    std::cout << "Hello World!\n";
}

32二叉树的层序遍历(不分行)

思路:

  1. 从根节点开始打印

  2. 对每个需要打印的节点,将其子节点放入队列中

  3. 打印完该节点,从队列头取出一个节点,回到2操作

  4. 直到队列为空

有向图的广度优先遍历也是用队列实现的,先把起始结点放入队列,然后从头部取出节点,遍历这个节点可以到达的所有节点且依次放入队列。重复这个遍历过程,直至全部遍历完

#include 
#include 
using namespace std;
struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

void printTree(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return;
    deque nodeQueue;
    nodeQueue.push_back(pRoot);
    while (!nodeQueue.empty())
    {
        BinaryNodeTree* node = nodeQueue.front();
        nodeQueue.pop_front();
        cout << node->m_nVal << " ";
        if (node->m_pLeft)
            nodeQueue.push_back(node->m_pLeft);
        if (node->m_pRight)
            nodeQueue.push_back(node->m_pRight);
    }
}

int main()
{
    std::cout << "Hello World!\n";
}

32二叉树的层序遍历(分行)

在上一题基础上,需要两个变量,v1记录本层未打印节点数,v2记录下层节点数

v1先置为1(根节点) v2置为0

根节点的子节点进入队列,v2++

根节点打印,v1–

v1==0时打印\n,然后执行v1 = v2, v2 = 0

重复

#include 
#include 
using namespace std;

struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

void printTree(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return;
    deque nodeQue;
    nodeQue.push_back(pRoot);
    int toBePrint = 1;
    int nextBePrint = 0;
    while (!nodeQue.empty())
    {
        BinaryNodeTree* node = nodeQue.front();
        nodeQue.pop_front();
        --toBePrint;
        if (node->m_pLeft)
        {
            nodeQue.push_back(node->m_pLeft);
            ++nextBePrint;
        }
        if (node->m_pRight)
        {
            nodeQue.push_back(node->m_pRight);
            ++nextBePrint;
        }
        cout << node->m_nVal << " ";
        if (toBePrint == 0)
        {
            cout << "\n";
            toBePrint = nextBePrint;
            nextBePrint = 90;
        }
    }
}
int main()
{
    std::cout << "Hello World!\n";
}

32二叉树层序遍历(之字形)

// offer32二叉树层序遍历(之字形).cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
using namespace std;

struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

void printTree(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return;
    stack stackDuet[2];
    int stackIndex = 0;
    stackDuet[stackIndex].push(pRoot);
    while (!stackDuet[0].empty() || !stackDuet[1].empty())
    {
        BinaryNodeTree* node = stackDuet[stackIndex].top();
        stackDuet[stackIndex].pop();
        cout << node->m_nVal << " ";
        if (stackIndex == 0)
        {
            if(node->m_pLeft)
                stackDuet[1].push(node->m_pLeft);
            if (node->m_pRight)
                stackDuet[1].push(node->m_pRight);
        }
        else
        {
            if (node->m_pRight)
                stackDuet[0].push(node->m_pRight);
            if (node->m_pLeft)
                stackDuet[0].push(node->m_pLeft);
            
        }
        if (stackDuet[stackIndex].empty())
        {
            ++stackIndex;
            stackIndex %= 2;
            cout << "\n";
        }
    }
}
int main()
{
   
    printTree(pRoot1);
    return 0;
}

33判断是否二叉搜索树的后序遍历序列

二叉搜索树的左子树比根小,右子树比根大

中序遍历是一个升序序列

思路:

后续遍历序列的最后一个节点为根,根的左子树全部比根小,右子树全部比根大,序列被分程两部分。

递归下去,直到所有子序列只有一个或0个数停止,返回true;

期间如果违背了后续遍历的根节点可将序列切分规则,则返回false;(例如找到序列中第一个比自己大的数,那么之前的数都是左子树,之后的数都是右子树,但如果右子树序列不是全部大于根节点则出错)

举一反三

如果要处理二叉树的遍历序列,先找到根节点,基于根节点把序列拆分成左右子树对应的子序列,然后递归地处理这两个子序列

#include 
using namespace std;
struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

bool isPostOrder(int* order, int length)
{
    if (!order || length <= 0)
        return false;
    int root = order[length - 1];
    int i = 0;
    while (order[i] > root)
        ++i;
    int j = i;
    while (j < length)
    {
        if (order[j] < root)
            return false;
    }
    bool left = true;
    if (i > 0)
        left = isPostOrder(order, i);
    bool right = true;
    if ((length-1-i) > 0)
        right = isPostOrder(order + i, length - 1 - i);
    return left && right;
}

int main()
{
    std::cout << "Hello World!\n";
}

34二叉树中和为某一值的路径

先序遍历的过程中,会从根节点触及到叶节点,然后触及另一个子树的叶节点。

采用栈去保留每一次根到叶的路径,如果路径和满足要求,那么打印这个栈。

不管满足与否触及到叶节点都要返回上面的节点,此时栈需要把叶节点pop出去

如果某一个节点的左右子树的路径都完成探索,那么这个节点也要pop出去

采用递归方式去探索,并且左右子树探索完毕,栈需要回退

调用递归的函数声明这个栈,在每次探索中都栈保存状态

#include 
#include 
using namespace std;
struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

void findPathCore(BinaryNodeTree* pRoot, vector path, int sum, int targetValue)
{
    path.push_back(pRoot->m_nVal);
    sum += pRoot->m_nVal;
    if (!pRoot->m_pLeft && !pRoot->m_pRight && sum == targetValue)
    {
        //auto iter = path.begin();
        vector::iterator iter = path.begin();
        for (; iter != path.end(); ++iter)
            cout << *iter << " ";
        cout << "\n";
    }
    if (pRoot->m_pLeft)
        findPathCore(pRoot->m_pLeft, path, sum, targetValue);
    if (pRoot->m_pRight)
        findPathCore(pRoot->m_pRight, path, sum, targetValue);
    path.pop_back();
}

void findPath(BinaryNodeTree* pRoot, int targetValue)
{
    if (!pRoot)
        return;
    vector path;
    int sum = 0;
    findPathCore(pRoot, path, sum, targetValue);
}

int main()
{
    std::cout << "Hello World!\n";
}

分治都可以用递归解决

35复杂链表的复制

  1. 复制原有节点,依次加入到原节点后面
  2. 复制sibling指针
  3. 分解链表

36二叉搜索树和双向链表

思路:

二叉搜索树的中序遍历正好是升序序列,使用中序遍历递归算法对每个节点进行转换

先设置一个ListLastNode节点(一开始值为null),始终指向链表中最大(最后)的一个节点。

每次遍历树节点时,对被操作的节点执行修改指针操作:left指向ListLastNode,ListLastNode指向被操作的节点,right设置为nullptr。然后把ListLastNode更新

注意失误

listLastNode是一个指针,不断最终递归过程,不能传值,要传指针***

// offer36二叉搜索树与双向链表.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;

struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

void convertCore(BinaryNodeTree* pRoot, BinaryNodeTree** pListLastNode)
{
    if (!pRoot)
        return;
    if (!pRoot->m_pLeft)
    {
        convertCore(pRoot, pListLastNode);
    }
    pRoot->m_pLeft = *pListLastNode;
    if (!(*pListLastNode))
    {
        (*pListLastNode)->m_pRight = pRoot;
    }
    *pListLastNode = pRoot;
    if (!pRoot->m_pRight)
    {
        convertCore(pRoot, pListLastNode);
    }
}

BinaryNodeTree* convert(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return nullptr;
    BinaryNodeTree* pListLastNode = nullptr;
    convertCore(pRoot, &pListLastNode);
    while (!pListLastNode->m_pLeft)
    {
        pListLastNode = pListLastNode->m_pLeft;
    }
    return pListLastNode;
}
int main()
{
    std::cout << "Hello World!\n";
}

37序列化和反序列化二叉树

无论序列化还是反序列化,都需要首先识别根节点,这样可以做到流处理,意味着先序遍历

对于空指针使用非数字字符代替,用于标记

// offer37序列化二叉树.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;
struct BinarNodeTree
{
    int m_nVal;
    BinarNodeTree* m_pLeft;
    BinarNodeTree* m_pRight;
};

void serialize(BinarNodeTree* pRoot, ostream& os)
{
    if (!pRoot)
    {
        os << "$" << " ";
        return;
    }
    os << pRoot->m_nVal << " ";
    serialize(pRoot->m_pLeft, os);
    serialize(pRoot->m_pRight, os);
}

void deSerialize(BinarNodeTree** ppRoot, istream& is)
{
    int number = 0;
    if (is >> number)
    {
        *ppRoot = new BinarNodeTree;
        (*ppRoot)->m_nVal = number;
        (*ppRoot)->m_pLeft = nullptr;
        (*ppRoot)->m_pRight = nullptr;

        deSerialize(&((*ppRoot)->m_pLeft), is);
        deSerialize(&((*ppRoot)->m_pRight), is);
    }
}
int main()
{
    BinarNodeTree* pRoot = nullptr;
    BinarNodeTree** ppRoot = &pRoot;
    deSerialize(ppRoot, cin);
    serialize(pRoot, cout);
}

38字符串的所有排列

将字符串分成首字符,和剩下字符串

  1. 首字符和剩下字符串的每一位进行交换

    ​ 1.注意,首字符也要和首字符进行交换,确保原排列也在

    1. 注意,每一位交换意味着循环,这个循环中交换了,要在下个循环恢复原样,让下个循环交换
  2. 交换后,剩下字符串重复以上过程

  3. 递归的出口是首字符是最后一个,那么直接打印即可出来这个递归栈的排列

// offer38字符串的排列.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;
void permutation(char* str, char* begin)
{
    if (*begin == '\0')
    {
        printf("%s\n", str);
        return;
    }
    for (char* pos = begin; *pos != '\0'; ++pos)
    {
        //首字符与后面每一个字符进行交换,包括交换他自己(自己也是排列中的一种)
        char temp = *begin;
        *begin = *pos;
        *pos = temp;
        //对后面的字符串进行相同操作
        permutation(str, begin + 1);
        //每次循环交换后需要恢复原样,为下一个循环的交换恢复现场
        temp = *begin;
        *begin = *pos;
        *pos = temp;
    }
}

int main()
{
    char str[1000] = { '\0' };
    cin >> str;
    permutation(str, str);
}

相关题目

立方体的排列,先求到所有排列,对每个排列检查是否符合立方体规则

8*8象棋的八个皇后:定义一个数组clos[8],表示每一行的皇后所处的列号,用0-7初始化数组;该数组必然是满足所有皇后不同行不同列的,但不满足不同对角线

是不是在相同对角线判别:对两个下标i、j,有i-j=clos[i]-clos[j]或者j-i=clos[i]-clos[j]

39求数组中出现次数超过一半的数组

解法1:

设定一个变量记录数字,另一个变量记录该数字的出现次数。

初始为第一个数字,记录数为1

遇见下一个数字相同加1,不同减1

如果遇见下一个数字时记录数为0,则替换数字,置记录数为1

最后记录数大于等于1的数有可能是出现次数超过>一半的数(记录数为0,则可能是出现次数=一半的数)(注意是可能)

通过一次遍历,判断是否是

解法2:基于partition

如果数字出现次数超过一半,那么排序之后,中位数一定是那个数字。(但中位数不一定出现次数超过一半)

随机选取一个数字做partition,如果该数字在中位,说明该数字是中位数,否则通过二分法查找中位数

查找到中位数再用一次遍历进行判断是否是

// offer39求数组中出现次数超过一半的数组.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

using namespace std;

bool checkInput(int* arr, int length)
{
    bool result = true;
    if (!arr || length <= 0)
        result = false;
    return result;
}

bool isMoreThanHalf(int* arr, int length, int number)
{
    int sum = 0;
    for (int cruisor = 0; cruisor < length; ++cruisor)
    {
        if (arr[cruisor] == number)
            ++sum;
    }
    if (sum > length >> 1)
        return true;
    return false;
}

int findMoreThanHalf_(int* arr, int length)
{
    if (!checkInput(arr, length))
        return -1;
    int number = arr[0];
    int times = 1;
    for (int i = 1; i < length; ++i)
    {
        if (times == 0)
        {
            number = arr[i];
            times = 1;
            continue;
        }
        if (arr[i] == number)
        {
            ++times;
        }
        else
        {
            --times;
        }
    }
    if (times >= 1)
    {
        if (isMoreThanHalf(arr, length, number))
            return number;
    }
    return -1;
}

int main()
{
    int arr[] = { 1, 2, 3, 3, 4, 1, 1 };

    std::cout << findMoreThanHalf_(arr, 7);
    return 0;
}

40 最小的K个数O(N)解法

解法1:O(n)的partition解法,但是会修改数组

使用partition的二分递归法,排列到index位k的坐标,左边的k个数是最小的

解法2:适合海量数据的O(nlogk)算法

选取一个最大堆,遍历n个数,如果堆中最大数大于被遍历的数,那么删除最大数,插入这个数。所以时间是O(nlogK)

一开始要建立这个堆。堆可以在O(logK)完成删除插入操作

可以使用红黑树来代替这个堆,红黑树的查找、删除和插入都是O(logK)

比如STL的set和mulitiset

multiset采用降序排列greater,这样就容器begin()就是最大值

// offer40最小的K个数.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
#include 
#include 
using namespace std;

typedef multiset> IntSet;

void findKMin(vector data, IntSet& minset, int k)
{
    minset.clear();
    if (k <= 0 || data.size() < k)
        return;
    auto iter = data.begin();
    for (; iter != data.end(); ++iter)
    {
        if (minset.size() < k)
        {
            minset.insert(*iter);
        }
        else
        {
            if (*(minset.begin()) > * iter)
            {
                minset.erase(minset.begin());
                minset.insert(*iter);
            }
        }
    }
}

int partition(int* arr, int length, int start, int end)
{
    if (!arr || length <= 0 || start < 0 || end > length - 1 || start > end)
        throw exception("invalid parameters.");
    int pilot = start;
    swap(arr[pilot], arr[end]);
    int small = start - 1;
    for (int index = start; index < end; ++index)
    {
        if (arr[index] < arr[end])
        {
            ++small;
            if (small != index)
                swap(arr[small], arr[index]);
        }
    }
    swap(arr[++small], arr[end]);
    return small;
}

void findKCore(int* arr, int length, int k)
{
    if (!arr || length <= 0 || k > length || k < 1)
        throw exception("invalid parameters.");
    int start = 0;
    int end = length - 1;
    int index = partition(arr, length, start, end);
    while (index != k - 1)
    {
        if (index < k - 1)
        {
            start = index + 1;
            index = partition(arr, length, start, end);
        }
        else
        {
            end = index - 1;
            index = partition(arr, length, start, end);
        }
    }
}

void findKmin(int* arr, int length, int k)
{
    if (!arr || length <= 0 || k <= 0 || k > length)
        return;
    findKCore(arr, length, k);
    for (int i = 0; i < k; ++i)
        cout << arr[i] << " ";
}

int main()
{
    int arr[] = { 100, -5, -8, 2, 10,
        32, 2, 54, 34, 22,
        786, 1, 0, -1, 8 };
    vector data;
    for (int i = 100; i > 0; --i)
    {
        data.push_back(i);
    }
    IntSet minSet;
    findKMin(data, minSet, 10);
    auto iter = minSet.begin();
    for (; iter != minSet.end(); ++iter)
        cout << *iter << " ";
    return 0;
}

41数据流中的中位数

将数据流流入两个容器,大堆装小的一半,小堆装大的一半

为了确保两个堆的数目最多相差1个数,规定,奇数放大堆,偶数放小堆

如果奇数时,该数大于小堆最小(大失误:!!!注意先检查该堆是否存在元素),那么先放入小堆,然后把小堆最小的数拿出来放入大堆

如果偶数时,该数小于大堆最大(大失误:!!!注意先检查该堆是否存在元素),那么先放入大堆,然后把大堆最大的数拿出来放入小堆

取中位数时,如果总数是奇数,那么大堆的数要多1,所以把大堆的最大数当作中位数;如果总数是偶数,那么大小堆各出一个数平均一下;

#include 
#include 
#include 
using namespace std;

template  class MiddleNum
{
public:
    void insert(T element)
    {
        int dataSum = maxHeap.size() + minHeap.size();
        ++dataSum;
        if ((dataSum & 1) == 1)//插入大堆
        {
            if (minHeap.size() > 0 && element > minHeap[0])//如果元素比小堆的顶部元素还要小
            {
                //把元素压入小堆
                minHeap.push_back(element);
                push_heap(minHeap.begin(), minHeap.end(), greater());
                //再从小堆取出顶部元素
                element = minHeap[0];
                //更新小堆
                pop_heap(minHeap.begin(), minHeap.end(), greater());
                minHeap.pop_back();
            }
            maxHeap.push_back(element);
            push_heap(maxHeap.begin(), maxHeap.end(), less());
        }
        else//插入小堆
        {
            if (maxHeap.size() > 0 && element < maxHeap[0])//如果元素比大堆的顶部元素还要大
            {
                //把元素压入大堆
                maxHeap.push_back(element);
                push_heap(maxHeap.begin(), maxHeap.end(), less());
                //再从大堆取出顶部元素
                element = maxHeap[0];
                //更新大堆
                pop_heap(maxHeap.begin(), maxHeap.end(), less());
                maxHeap.pop_back();
            }
            minHeap.push_back(element);
            push_heap(minHeap.begin(), minHeap.end(), greater());
        }
    }
    T getMiddleNum()
    {
        int dataSum = maxHeap.size() + minHeap.size();
        if (dataSum == 0)
            throw exception("have no data in MiddleNum.");
        T middle = 0; 
        if ((dataSum & 1) == 1)//大堆的最大元素是中位数
        {
            return maxHeap[0];
        }
        else//中位数需要两个堆各取出一个,取平均值
        {
            return (maxHeap[0] + minHeap[0])/2;
        }
    }
private:
    vector maxHeap;
    vector minHeap;
};

int main()
{
    MiddleNum contener;
    int number = 0;
    while (cin >> number)
    {
        contener.insert(number);
    }

    std::cout << contener.getMiddleNum() << endl;
    return 0;
}

STL堆算法

堆是一个近乎完全的完全二叉树,所以可以按照层序放入数组中存储;

vector和pop_heap(begin, end, greater<>(小堆) | less<>(大堆))、push_heap(begin, end, greater<>(小堆) | less<>(大堆))可以维护一个大堆或者小堆

以大堆距离

入堆

vector.push_back(element);//压入数组的末尾

push_heap(vector.begin(), vector.end(), less());使用函数调整维护大堆

出堆

int ele = vector[0];//是这样取堆顶元素的

pop_heap(vector.begin(), vector.end(), less());//把大堆中的根节点和最后的末尾节点对调,然后调整维护除末尾以外的堆

vector.pop_back();//把末尾原本是大堆堆顶的元素时放掉

42连续子数组的最大和

思路:O(n)解法

从第一个元素开始相加,记录当前sum值

循环对下一个元素进行相加,在相加之前,

如果sum<=0,说明之前的子数列完全没有作用,将之舍弃,令sum等于下一个元素

如果sum>0, 则sum+下一个元素

在循环过程中记录sum出现的最大值

补漏:

int 最大值 (1<<31)-1 INT_MAX

int最小值 1<<31 INT_MIN

// offer42数组中最大子序列和.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;

bool isInputValid = true;

int getMaxSum(int* arr, int length)
{
    isInputValid = true;
    if (!arr || length <= 0)
    {
        isInputValid = false;
        return 0;
    }
    int sum = 0;
    int maxsum = INT_MIN;
    for (int cruisor = 0; cruisor < length; ++cruisor)
    {
        if (sum <= 0)
            sum = arr[cruisor];
        else
            sum += arr[cruisor];
        if (sum > maxsum)
            maxsum = sum;
    }
    return maxsum;
}

int main()
{
    int arr[] = { 1, -2, 3, 10, -4, 7, 2, -5 };

    std::cout <

43 1~n整数中1出现的次数

44 数字序列中某一位数字

思路:

数字是有规律的

1位数有10个,二位数90个,三位数900个。。。

数字序列某一位n,那么这个数字序列一定要满足N位数的序列长度大于n,自此可以定位到这个n序列数是几位数,在几位数中的序列是排第m位

而几位数的数字长度是固定的k,m/k就是几位数的第几个数

m%k就是这个数出现的第几位

函数:

判断index指向的会是几位数

统计几位数一共有多少个数

知道index指向于几位数时,找出那个数,并指向那个数的对应数字

几位数的起始数是啥

// offer44数字序列中某一位数字.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

int digitNumberBegin(int digit)
{
    if (digit == 1)
        return 0;
    return (int)pow(10, digit - 1);
}

int digitNumberCount(int digit)
{
    if (digit == 1)
        return 10;
    return 9 * (int)pow(10, digit - 1);
}

int numberIndex(int index, int digit)
{
    int number = digitNumberBegin(digit) + (index + 0) / digit;
    int indexFromRight = digit - (index + 0) % digit;
    for (int i = 1; i < indexFromRight; ++i)
        number /= 10;
    return number % 10;
}

int getNumberIndex(int index)
{
    if (index < 0)
        return -1;
    int digit = 1;
    while (true)
    {
        if (digitNumberCount(digit)*digit > index)
            return numberIndex(index, digit);
        index -= digitNumberCount(digit) * digit;
        digit++;
    }
}

int main()
{
    std::cout << getNumberIndex(19);
}

45把数组排成最小的数

核心,对于数字m和n,如果mn > nm 则说明m > n,要排得最小数n必须在前面。

可以考虑使用字符串比较来比较大小,对数字序列进行排序,升序排序就是最小的数。

1.声明一个字符串数组,每个元素是char*,用于存每个数的字符串

2.使用qsort对字符串数组排序,需要编写cmp函数

3.cmp函数比较字符串mn大还是nm大

失误:

不要忘记释放动态内存

小技巧:

char** numberStrArray = (char**)new int[length];//申请存放字符指针的数组

int compare(const void * a, const void * b)//qsort cmp
{
    return (*(int *)a - *(int *)b);//不能直接是(int)a
    return strcpy(g_comcatStr1, *(const char**)str1);//不能直接是char*
}

TIPs:

qsort头文件stdlib.h

strcat/strcmp/strcpy头文件string.h

memset memcpy头文件string.h

sprintf头文件stdio.h

#include 
#include 
#include 
#include 

using namespace std;

const int stringSize = 10;
char g_comcatStr1[2 * stringSize + 1];
char g_comcatStr2[2 * stringSize + 1];

int strcatcmp(const void* str1, const void* str2)
{
    strcpy(g_comcatStr1, *(const char**)str1);
    strcat(g_comcatStr1, *(const char**)str2);

    strcpy(g_comcatStr2, *(const char**)str2);
    strcat(g_comcatStr2, *(const char**)str1);
    return strcmp(g_comcatStr1, g_comcatStr2);
}

void minNumber(int* numbers, int length)
{
    if (!numbers || length <= 0)
        return;
    char** numberStrArray = (char**)new int[length];
    for (int i = 0; i < length; ++i)
    {
        numberStrArray[i] = new char[stringSize + 1];
        sprintf(numberStrArray[i], "%d", numbers[i]);
    }
    qsort(numberStrArray, length, sizeof(char*), strcatcmp);
    for (int i = 0; i < length; ++i)
        printf("%s", numberStrArray[i]);
    printf("\n");

    for (int i = 0; i < length; ++i)
        delete[] numberStrArray[i];
    delete[] numberStrArray;
}



int main()
{
    int arr[] = { 1, 2, 3 , 4, 5, 6, 7, 8 };
    minNumber(arr, 8);
    return 0;
}

46把数字翻译成字符串

可以递归进行翻译,但是会出现重复子问题,重复子问题在数字串的后面出现

可以从数字末尾开始翻译,并把子问题存储起来。

递归方法:

如果连续两个字符是10~25,那么可以分成两个字母表示或者组合成一个字符表示

如果连续两个字符无法满足10~25,那么只能分成两个字母表示

出口是index走完字符串,返回1,表示一种表达方式

// offer46把数字翻译成字符串.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
#include 
using namespace std;
int translateCore(char* numberStr, int length, int index)
{
    if (index > length - 1)
        return 1;//如果走到底,说明这种表述方法加1
    if (index + 1 <= length - 1)
    {
        int val = (numberStr[index] - '0') * 10 + (numberStr[index + 1] - '0');
        if (val >= 10 && val <= 25)//一种情况
            return translateCore(numberStr, length, index + 1) + translateCore(numberStr, length, index + 2);
    }
    return translateCore(numberStr, length, index + 1);//另一种情况
     
}

void translate(int number)
{
    char numberStr[10];
    sprintf_s(numberStr, "%d", number);
    int length = strlen(numberStr);
    cout << translateCore(numberStr, length, 0);
}

int main()
{
    translate(12258);
}

动态规划:

自下而上,动态规划,从最小的问题开始 :
f®表示以r为开始(r最小取0)到最右端所组成的数字能够翻译成字符串的种数。对于长度为n的数字,f(n)=0,f(n-1)=1,求f(0)。
递推公式为 f(r-2) = f(r-1)+g(r-2,r-1)*f®;
其中,如果(r-2,r-1)组合起来能够翻译成字符,则g(r-2,r-1)=1,否则为0。

比如1 5 8中1 和 5字符可以组合,所以f(字符1) = f(字符5——不组合) + f(字符8–15组合)

int translate_dp(int number)
{
    if (number < 0)
        return -1;
    string numberStr = to_string(number);
    int length = numberStr.length();
    int* count = new int[length + 1];
    count[length] = 0;
    count[length - 1] = 1;
    for (int index = length - 2; index >= 0; --index)
    {
        int number1 = numberStr[index] - '0';
        int number2 = numberStr[index + 1] - '0';
        int numberComcat = number1 * 10 + number2;
        if (numberComcat >= 10 && numberComcat <= 25)
            count[index] = count[index + 1] + count[index + 2];
        else
            count[index] = count[index + 1];
    }
    return count[0];
}

47礼物的最大价值

递归:

int maxValue(int* gift, int rows, int cols, int row, int col)
{
    if (!gift || rows <= 0 || cols <= 0 || row < 0 || col < 0 || row > rows - 1 || col > cols - 1)
        throw exception("invalid parameter.");
    if (row == 0 && col == 0)//递归的最底层
        return gift[0];

    int max1 = INT_MIN;
    int max2 = INT_MIN;
    if (row - 1 >= 0)
        max1 = maxValue(gift, rows, cols, row - 1, col);
    if (col - 1 >= 0)
        max2 = maxValue(gift, rows, cols, row, col - 1);
        
    return gift[row * cols + col] + (max1 > max2 ? max1 : max2);//递归出口
}

动态规划:

定义一个函数f(i, j)表示到达坐标位ij的格子是能拿到的礼物总和的最大值

f(i, j) = max{f(i-1, j), f(i, j-1) + gift[i,j]}

借助辅助二维数组,缓存中间计算结果

48最长不含重复字符的子字符串

动态规划:

记f(i)为以i下标的字符结尾的最长不含重复字符的字符串长度

如果第i个字符在之前的扫描中(0---- i-1)没有出现,则f(i)=f(i-1)+1

如果第i个字符在之前的扫描中(0---- i-1)出现过,假设出现的字符到i字符之间的下标距离为d

  1. 如果出现在f(i)所代表的字符串中(d<=f(i-1)),f(i) = d
  2. 如果出现在f(i)字符串之外(d>f(i-1)),f(i+1)=f(i)+1
int maxNoDupStrLen(const string& str)
{
    int length = str.length();
    if (length == 0)
        return 0;
    int currentLen = 0;
    int maxLen = 0;
    int* letterIndexTable = new int[26];
    for (int i = 0; i < 26; ++i)
        letterIndexTable[i] = -1;
    for (int index = 0; index < length; ++index)
    {
        int letter = str[index] - 'a';
        int preIndex = letterIndexTable[letter];
        letterIndexTable[letter] = index;
        int distanceOfLetter = index - preIndex;
        if (preIndex < 0 || distanceOfLetter > currentLen)
            ++currentLen;
        else
        {
            //最容易犯错的地方
            if (currentLen > maxLen)
                maxLen = currentLen;
            currentLen = distanceOfLetter;
        }
    }
    //最容易犯错的地方
    if (currentLen > maxLen)
        maxLen = currentLen;
    delete[] letterIndexTable;
    return maxLen;
}

49丑数

丑数的定义: 只能被2, 3, 5整除为1的数,即因数只包含2, 3, 5

bool isUglyNumber(int number)
{
    while (number % 2 == 0)
        number /= 2;
    while (number % 3 == 0)
        number /= 3;
    while (number % 5 == 0)
        number /= 5;
    return number == 1;
}

空间换时间的解法

核心:丑数是另一个丑数乘以2/3/5的结果

确保只计算丑数不计算非丑数

使用数组保存已找到的丑数(已排序,其中最大丑数是M),计算下一个丑数时,必然是之前的某个丑数分别乘以2,3,5的结果,分别得到三个第一个大于M2、M3、M5的丑数即可,从M2、M3、M5选取最小数加入数组

// offer49丑数.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

int minUgly(int a, int b, int c)
{
    int d = (a < b) ? a : b;
    return (c < d) ? c : d;
}

int getUglyNumber(int index)
{
    if (index <= 0)
        return -1;
    int* uglyNumbers = new int[index];
    uglyNumbers[0] = 1;
    int nextIndex = 1;
    int* pMulti2 = uglyNumbers;
    int* pMulti3 = uglyNumbers;
    int* pMulti5 = uglyNumbers;
    
    while (nextIndex < index)
    {
        int min = minUgly(*pMulti2 * 2, *pMulti3 * 3, *pMulti5 * 5);
        uglyNumbers[nextIndex] = min;
        //pMulti2指针无须重置,因为前面遍历的三个指针必然无法超越最新的丑数,
//        经过循环后是第一个超过最新丑数的下表
        while (*pMulti2 * 2 <= uglyNumbers[nextIndex])
            ++pMulti2;//更新除第一个*pMulti2 * 2会大于最新丑数的下标
        while (*pMulti3 * 3 <= uglyNumbers[nextIndex])
            ++pMulti3;
        while (*pMulti5 * 5 <= uglyNumbers[nextIndex])
            ++pMulti5;
        ++nextIndex;
    }
    int result = uglyNumbers[index - 1];
    delete[] uglyNumbers;
    return result;
}

int main()
{
    std::cout << getUglyNumber(1500);
    return 0;
}

50 第一次只出现一次的字符

hashtable解决

类似问题有

  1. 输入两个字符串,从第一个字符串删除第二个字符串中出现的所有字符
  2. 删除字符中所有重复出现的字符
    1. 定义一个bool型hash表,全部置为false,扫面到一个字符就置为true,下次再扫描到直接放掉
  3. 判断互位词,如果单词字母数相同,字母也相同,只是排列不一样
    1. 定义一个int型hash表,置0,记录单词1每个字符出现次数,对单词2扫描遇到每个字符对hash表减1,查看最后的hash表是否全为0
  4. 给出字符流中第一个只出现1次的字符
    1. 给定一个int型hash表,置-1,记录流中字符出现的位置,如果下一个位置还是出现相同字符,对应hash表置为-2,不再更新
    2. 扫面hash表,所有>=0的值,最小的那个值就是第一个只出现一次的字符的位置
#include 
#include 
#include 
using namespace std;

class GetFirstOnceInStream
{
public:
    GetFirstOnceInStream()
    {
        for (int i = 0; i < 256; ++i)
            hashTable[i] = -1;
    }
    void insert(char& ch)
    {
        charStream.push_back(ch);
        if (hashTable[ch] != -2)
        {
            if (hashTable[ch] == -1)
                hashTable[ch] = charStream.size() - 1;
            else
                hashTable[ch] = -2;
        }
    }
    char getFirstOnce()
    {
        int minIndex = INT_MAX;
        char ch = '\0';
        for (int i = 0; i < 256; ++i)
        {
            if (hashTable[i] >= 0 && minIndex > hashTable[i])
            {
                ch = (char)i;
                minIndex = hashTable[i];
            }

        }
        return ch;
    }
private:
    int hashTable[256];
    vector charStream;
};

char* getNoDupStr(const char* pString1, const char* pString2)
{
    if (!pString1 || !pString2)
        return nullptr;
    char* newString = new char[strlen(pString1) + 1];
    char hashTable[256] = { 0 };
    const char* cruisor = pString2;
    while (*cruisor != '\0')
        hashTable[*(cruisor++)]++;
    char* writer = newString;
    cruisor = pString1;
    while (*cruisor != '\0')
    {
        if (hashTable[*cruisor])
        {
            cruisor++;
            continue;
        }
        *writer = *cruisor;
        writer++;
        cruisor++;
    }
    *writer = '\0';
    return newString;
}

char findFirestOnlyOnce(const char* pString)
{
    if (!pString)
        return -1;
    char hastable[256] = { 0 };
    const char* cruisor = pString;
    while (*cruisor != '\0')
        hastable[*(cruisor++)]++;
    cruisor = pString;
    while (*cruisor != '\0')
    {
        if (hastable[*(cruisor++)] == 1)
            break;
    }
    if (*cruisor == '\0')
        return -1;
    return *(cruisor - 1);
}

int main()
{
    cout << findFirestOnlyOnce("abaccdeff") << endl;
    cout << getNoDupStr("We are students.", "aeiou") << endl;
    char ch;
    GetFirstOnceInStream gfois;
    while (cin >> ch)
    {
        gfois.insert(ch);
    }
    cout << gfois.getFirstOnce() << endl;
    cin.clear();
    while (cin >> ch)
    {
        gfois.insert(ch);
    }
    cout << gfois.getFirstOnce() << endl;
    return 0;
}

51统计数组逆序对-归并排序

核心:归并排序

思路:先把数组分割成子数组,统计子数组内部逆序对,在统计两个相邻子数组的逆序对,统计过程中对合并数组进行归并排序

代码技巧:

copy和data两个数组轮换着使用

失误:

  1. delete[] 忘记

  2. 二分法的时候start end len = (end - start)/2

    此时[start, start + len - 1]会出错,导致越界,可能新end比start还要小,因为len=0

// offer51数组中的逆序对.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 

int inversePairCore(int* data, int* copy, int start, int end)
{
    if (start == end)
    {
        copy[start] = data[start];
        return 0;
    }
    int halfCutLen = (end - start) / 2;
    int left = inversePairCore(copy, data, start, start + halfCutLen);
    int right = inversePairCore(copy, data, start + halfCutLen + 1, end);
    int leftCruisor = start + halfCutLen;
    int rightCruisor = end;
    int copyCruisor = end;
    int count = 0;
    while (start <= leftCruisor && (start + halfCutLen + 1) <= rightCruisor)
    {
        if (data[leftCruisor] > data[rightCruisor])
        {
            count += rightCruisor - start - halfCutLen;
            copy[copyCruisor--] = data[leftCruisor--];
        }
        else
        {
            copy[copyCruisor--] = data[rightCruisor--];
        }
    }
    while (start <= leftCruisor)
        copy[copyCruisor--] = data[leftCruisor--];
    while ((start + halfCutLen + 1) <= rightCruisor)
        copy[copyCruisor--] = data[rightCruisor--];
    return count + left + right;
}

int inversePair(int* data, int length)
{
    if (!data || length <= 0)
        return -1;
    int* copy = new int[length];
    for (int i = 0; i < length; ++i)
        copy[i] = data[i];
    int count = inversePairCore(data, copy, 0, length - 1);
    delete[] copy;
    return count;
}

int main()
{
    int arr[] = { 7, 5, 6, 4 };
    std::cout << inversePair(arr, 4);
    return 0;
}

52两个链表的第一个公共节点

方法1:用两个栈存储两个链表的下一个节点的指针,从栈顶开始比较每个节点是否相同,知道找到最后一个相同的节点

方法2:统计两个链表长度,从相同长度地方开始遍历,第一个next相同的地方,next所指的就是第一个公共节点

// offer52链表的第一个公共节点.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;

struct ListNode
{
    int m_nVal;
    ListNode* m_pNext;
};

int lengthOfList(ListNode* pHead)
{
    if (!pHead)
        return -1;
    int length = 0;
    ListNode* pCruisor = pHead;
    while (pCruisor != null)
    {
        ++length;
        pCruisor = pCruisor->m_pNext;
    }
    return length;
}

ListNode* firstComNode(ListNode* pHead1, ListNode* pHead2)
{
    if (!pHead1 || !pHead2)
        return nullptr;
    int length1 = lengthOfList(pHead1);
    int length2 = lengthOfList(pHead2);
    ListNode* pCruisor1 = pHead1;
    ListNode* pCruisor2 = pHead2;

    if (length1 > length2)
    {
        for (int i = 0; i < length1 - length2; ++i)
            pCruisor1 = pCruisor1->m_pNext;
    }
    if (length2 > length1)
    {
        for (int i = 0; i < length2 - length1; ++i)
            pCruisor2 = pCruisor2->m_pNext;
    }
    while (pCruisor1 != pCruisor2)
    {
        pCruisor1 = pCruisor1->m_pNext;
        pCruisor2 = pCruisor2->m_pNext;
    }
    return pCruisor1;
}

int main()
{
    std::cout << "Hello World!\n";
}

53统计数字在排序数组中出现的次数

O(logN)的办法:通过二分法查找第一个K和最后一个K

查找第一个K:每次从中间取一个数n,如果nk说明k在左半部分;如果n=k且k的前面一个数字不等于k说明找到第一个k,如果n=k且前面一个数等于k说明第一个k在左半部分

如果查找不到k(start > end)return -1;

53查找0~n-1中缺失的数字

方法1:利用公式n(n-1)/2求出数字0到n-1的所有数字之和s1,再遍历求出所有数字s2,缺失数字就是s1-s2

方法2:利用递增的性质,数组一开始的数字和下标是相同的,直到不在数组中的数,后面的数字比下标大1;

基于二分法,如果中间元素值和下标相等,在右边查找;如果中间元素值与下标不等

  • 如果左旁边的元素值与下表不等,那么在左边查找
  • 如果左旁边的元素值与下标相等,那么找到了,中间元素值减1即可
// offer53查到0~n-1中缺失的数字.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;

int getMissingNumberCore(int* numbers, int length)
{
    if (!numbers || length <= 0)
        return -1;
    int start = 0;
    int end = length - 1;
    while (start <= end)
    {
        int middle = (start + end) >> 1;
        if (middle == numbers[middle])
        {
            start = middle + 1;
        }
        else
        {
            if (middle == start || (middle - 1) == numbers[middle - 1])
                return middle;
            else
                end = middle - 1;
        }
    }
    return -1;
}

int main()
{
    int arr[] = { 0, 1, 2, 3, 4, 5, 7, 8 };
    int arr1[] = { 0, 1, 2, 3, 4, 5, 6, 7, 9 };
    std::cout << getMissingNumberCore(arr1
        , 9);
}

53查找递增数组中数值与下标相等的元素

普通方法:遍历

二分法:

如果middle与元素相等返回,

如果middle大于元素,查找右边

如果middle小于元素,查找左边

// offer53查找升序数组中等于下标的元素.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;

int getElementAsIndex(int* number, int length)
{
    if (!number || length <= 0)
        return -1;
    int start = 0;
    int end = length - 1;
    while (start <= end)
    {
        int middle = (start + end) >> 1;
        if (middle == number[middle])
            return middle;
        else if (number[middle] > middle)
            end = middle - 1;
        else
            start = middle + 1;
    }
    return -1;
}

int main()
{
    int numbers[] = { -3, -1, 1, 3, 5 };
    std::cout << getElementAsIndex(numbers, 5);
}

54 查找二叉搜索树的第K大节点

???挺难的

BinaryNodeTree* findKNodeCore(BinaryNodeTree* pRoot, int& k)
{
    BinaryNodeTree* target = nullptr;
    if (pRoot->m_pLeft != nullptr)
        target = findKNodeCore(pRoot->m_pLeft, k);
    if (target == nullptr)
    {
        if (k == 1)
            target = pRoot;
        k--;
    }
    if (target == nullptr && pRoot->m_pRight != nullptr)
        target = findKNodeCore(pRoot->m_pRight, k);

    return target;
}

55 二叉树的深度

如果一颗树只有一个节点,那么他的深度1

如果一棵树只有左子树,那么深度为1+左子树深度

如果一棵树只有右子树,那么深度为1+右子树深度

如果一棵树有左右子树,那么深度为1+左子树深度、1+右子树深度的最大值

int depthOfTree(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return 0;
    //if (pRoot->m_pLeft == nullptr && pRoot->m_pRight == nullptr)
        //return 1;
    int left = depthOfTree(pRoot->m_pLeft);
    int right = depthOfTree(pRoot->m_pRight);
    return 1 + ((left > right) ? left : right);
}

55判断是否为平衡二叉树

普通方法:造成节点被重复遍历

遍历树的每个节点时,调用树深度函数,得到左右子树的深度,如果每个节点的左右子树深度不超过1,则为平衡二叉树

只遍历一次的方法:

后序遍历,遍历某个节点前已经遍历到它的左右子树,记录该节点深度

// offer55判断平衡二叉树.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;
struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};
int depthOfTree(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return 0;
    //if (pRoot->m_pLeft == nullptr && pRoot->m_pRight == nullptr)
        //return 1;
    int left = depthOfTree(pRoot->m_pLeft);
    int right = depthOfTree(pRoot->m_pRight);
    return 1 + ((left > right) ? left : right);
}

bool isBalanceTree(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return false;
    int diff = depthOfTree(pRoot->m_pLeft) - depthOfTree(pRoot->m_pRight);
    if (diff > 1 || diff < -1)
        return false;
    bool left = isBalanceTree(pRoot->m_pLeft);
    bool right = isBalanceTree(pRoot->m_pRight);
    return left && right;
}

bool isBalanceTreePost(BinaryNodeTree* pRoot, int* pDepth)
{
    if (!pRoot)
    {
        *pDepth = 0;
        return true;
    }
    int left = 0;
    int right = 0;
    if (isBalanceTreePost(pRoot->m_pLeft, &left) && isBalanceTreePost(pRoot->m_pRight, &right))
    {
        int diff = left - right;
        if (diff >= -1 && diff <= 0)
        {
            *pDepth = (left > right) ? left : right;
            return true;
        }
    }
    return false;
}

int main()
{
    std::cout << "Hello World!\n";
}

55树的深度

// offer55树的深度.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;

struct BinaryNodeTree
{
    int m_nVal;
    BinaryNodeTree* m_pLeft;
    BinaryNodeTree* m_pRight;
};

int depthOfTree(BinaryNodeTree* pRoot)
{
    if (!pRoot)
        return 0;
    //if (pRoot->m_pLeft == nullptr && pRoot->m_pRight == nullptr)
        //return 1;
    int left = depthOfTree(pRoot->m_pLeft);
    int right = depthOfTree(pRoot->m_pRight);
    return 1 + ((left > right) ? left : right);
}
int main()
{
}

56数组中只出现一次的两个数字

要求时间复杂度O(N),空间复杂度O(1)

异或:两个一样的数异或结果为零

加入数组中只有一个是只出现一次的数字,其他都是两个,那么所有数字参与异或,结果是那个只出现一次的数字

解法:如果将数组分为两个数组,各自有一个只出现一次的数字,各自有一对出现的数字(一对数字不能被分在两个数组里)

将所有数异或,对结果中的某一位的1进行研究

必然只能是只出现一次的两个数中的一个数满足某一位为1。

所以按照某一位是否为1的做法可以把数组分成两个数组,两个数分别处在不同数组;而且,每一对数必然被分配到同一数组

但是两个数组不一定要分配内存,可以直接做异或

// offer56数组中只出现一次的两个数字.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;

int findFirstBit1Index(int number)
{
    int index = 0;
    int pilot = 1;
    while (number & pilot == 0)
    {
        pilot = pilot << 1;
        index++;
    }
    return index;
}

bool isBit1AtIndex(int number, int index)
{
    int pilot = 1;
    pilot = pilot << index;
    return number & pilot;
}

bool find2OnlyOnce(int* numbers, int length, int* number1, int* number2)
{
    if (!number1 || length <= 0)
        return false;
    int sum = 0;
    for (int i = 0; i < length; ++i)
        sum ^= numbers[i];
    int indexOfBit1 = findFirstBit1Index(sum);
    for (int i = 0; i < length; ++i)
    {
        if (isBit1AtIndex(numbers[i], indexOfBit1))
            *number1 ^= numbers[i];
        else
            *number2 ^= numbers[i];
    }
    return true;
}

int main()
{
    int arr[] = { 1, 1, 2, -3, 2, 4, 5, 5, 6, 6, 89, 89, -1, -1, -100, -100 };
    int number1 = 0;
    int number2 = 0;
    find2OnlyOnce(arr, 16, &number1, &number2);
    std::cout << number1 << " " << number2;
    return 0;
}

56数组唯一只出现一次的数字(其他出现了三次)

采取位运算,所有位都加起来存在对应数组,每一位%3的结果就是唯一一次出现的数字

int findOnlyOnceNumber(int* numbers, int length)
{
    if (!numbers || length <= 0)
        throw exception("invalid parameters.");

    int bitArray[32] = { 0 };
    for (int i = 0; i < length; ++i)
    {
        int maskBit1 = 1;
        for (int j = 31; j >= 0; --j)
        {
            //大失误
            //bitArray[j] += numbers[i] & maskBit1;
            int bit = numbers[i] & maskBit1;
            if (bit != 0)
                bitArray[j] += 1;
            maskBit1 = maskBit1 << 1;
        }
    }
    int numberOnlyOnce = 0;
    for (int i = 0; i < 32; ++i)
    {
        numberOnlyOnce = numberOnlyOnce << 1;
        numberOnlyOnce += bitArray[i] % 3;
    }
    return numberOnlyOnce;
}

57和为S的两个数字(在一个递增数组中的)O(N)

暴力法:两层遍历O(N^2)

头尾指针法:O(N)

p1指向第一个元素,p2指向最后一个元素

p1+p2的和大于S,p2–

p1+p2的和大于S,p1++

#include 
using namespace std;

void printSerials(int small, int big)
{
    if (small > big)
        return;
    for (int i = small; i <= big; ++i)
        cout << i << " ";
    cout << " ";
}

int serialsSum(int small, int big)
{
    if (small > big)
        return -1;
    int sum = 0;
    for (int i = small; i <= big; ++i)
        sum += i;
    return sum;
}

void findSerials(int sum)
{
    if (sum < 3)
        return;
    int small = 1;
    int big = 2;
    int smallEnd = (sum + 1) / 2;
    while (small < big && small < smallEnd)
    {
        int curSum = serialsSum(small, big);
        if (curSum == sum)
        {
            printSerials(small, big);
            ++big;
        }
        else if (curSum > sum)
            ++small;
        else
            ++big;
    }
}

int main()
{
    findSerials(15);
    return 0;
}

57和为S的连续数字序列(从1开始)

设置small、big分别表示序列中最小值和最大值

初始small=1 big = 2

限制small < big small < (1+sum)/2

如果序列和大于sum,small前进一位

如果序列和小于sum,big前进一位

如果符合,打印,同时big++,寻找下一个序列

直到限制条件

void findSerials(int sum)
{
    if (sum < 3)
        return;
    int small = 1;
    int big = 2;
    int smallEnd = (sum + 1) / 2;
    while (small < big && small < smallEnd)
    {
        int curSum = serialsSum(small, big);
        if (curSum == sum)
        {
            printSerials(small, big);
            ++big;//大失误,容易漏掉
        }
        else if (curSum > sum)
            ++small;
        else
            ++big;
    }
}

58反转单词顺序

基本:反转字符串,头尾指针交换字符反转

方法:

  1. 先反转整个字符串
  2. 再以空格为界反转每个连续的单词

单词空格分界

  1. begin = end = str
  2. 如果遇到空格,begin和end一起++
  3. 如果end遇到空格或\0,那么begin和end-1之间是一个单词
  4. 其他情况,end++
  5. 以begin!=\0为终止
void wordReverse(char* str)
{
    if (!str)
        return;
    char* pBegin = str;
    char* pEnd = str + strlen(str) - 1;
    strReverse(pBegin, pEnd);
    cout << str << endl;
    pBegin = pEnd = str;
    while (*pBegin != '\0')
    {
        if (*pBegin == ' ')
        {
            pBegin++;
            pEnd++;
        }
        else if (*pEnd == ' ' || *pEnd == '\0')//最容易忽视的地方
        {
            strReverse(pBegin, --pEnd);
            pBegin = ++pEnd;
        }
        else
        {
            pEnd++;
        }
    }
}

58左旋转字符串

先分段反转,再整体反转

59队列的最大值–滑动窗口的最大值

1.暴力法:扫描每个滑动窗口数字,窗口大小为k,O(nk)

2.队列用栈实现:

滑动窗口是一个队列,如果可以在O(1)时间就找到队列最大值,那么复杂度就是O(N)

队列用两个栈实现可以达到目的

3.只把滑动窗口最大值存入队列:

使用两端开口的deque,保存有可能是滑动窗口最大值的数字的下标

  1. 如果新来的值比队列尾部的数小,那就追加到后面,因为它可能在前面的最大值划出窗口后成为最大值
  2. 如果新来的值比尾部的大,那就删掉尾部,再追加到后面
  3. 如果追加的值比的索引跟队列头部的值的索引超过窗口大小,那就删掉头部的值
  4. 每次队列的头都是滑动窗口中值最大的
template  vector maxInWindow(vector numbers, int windowSize)
{
    if (numbers.size() <= 0 || windowSize <= 0 || numbers.size() < windowSize)
        throw exception("invlid parameters.");
    vector maxSerials;
    deque window;
    for (int i = 0; i < numbers.size(); ++i)
    {
        //开头需要三个数字填充窗口
        if (i < windowSize)
        {
            //不可能成为最大值的去除
            while (!window.empty() && numbers[i] > numbers[window.back()])
                window.pop_back();

            //压入窗口
            window.push_back(i);
            continue;
        }
        //压入窗口前,把当前窗口最大值记录下来
        maxSerials.push_back(numbers[window.front()]);
        //不可能成为最大值的去除
        while (!window.empty() && numbers[i] > numbers[window.back()])
            window.pop_back();
        //压入窗口
        window.push_back(i);
        //删除旧窗口数据
        if (i - window.front() >= windowSize)
            window.pop_front();
    }
    //容易犯错漏掉,最后一个窗口的max
    maxSerials.push_back(numbers[window.front()]);
    return maxSerials;
}

59队列的最大值–定义一个队列,并实现max函数O(1)

  1. 使用一个辅助队列专门存放当前队列的最大值
  2. 入队队列时
while (!window.empty() && number > window.back())
                window.pop_back();
  1. 出队列时,要考虑到辅助队列的最大元素可能被弹出
    1. 出于这个考虑,要使用下标记录弹出的元素,如果元素相同,那么需要弹出最大值
    2. 所以对于每一个队列操作的数字都要加上一个ID,即下标用于唯一标志
template  class dequeWithMax
{
public:
    dequeWithMax():id(-1){}
    void push_bakc(T element)
    {
        id++;
        while (!maxque.empty() && element > maxque.back())
            maxque.pop_back();
        Mystruct data = { id, element };
        maxque.push_back(data);
        myque.push_back(data);
    }
    void pop_front()
    {
        if (myque.empty())
            throw new exception("queque is empty.");
        if (maxque.front().id == myque.front().id)
            maxque.pop_front();
        myque.pop_front();
    }
    T max() const
    {
        if(myque.empty())
            throw new exception("queque is empty.");
        return maxque.front().element;
    }
private:
    struct Mystruct
    {
        //每一个元素的唯一ID
        int id;
        T element;
    };
    int id;
    deque myque;
    deque maxque;
};

60n个色子的点数之和s的概率

色子点数16,n和色子的和值为n6n,所有点数和的情况有6n种,统计出每个和值出现的次数,除以6n

递归求解:

n和色子分为,1和n-1,先求1个塞子点数,再求其他塞子点数,用hashtable保存每一种情况出现的次数

// offer60N个色子的的点数和出现的概率.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
#include 
using namespace std;

const int maxOfRoll = 6;

void rockRoll(int sumOfRoll, int currentRoll, int sum, int* allSums)
{
    if (sumOfRoll <= 0 || currentRoll <= 0 || currentRoll > sumOfRoll ||
        allSums == nullptr)
        throw new exception("invlid parameters.");
    if (currentRoll == sumOfRoll)
    {
        for (int i = 1; i <= maxOfRoll; ++i)
        {
            int finalSum = sum + i;
            allSums[finalSum - sumOfRoll]++;
        }
    }
    else
    {
        for (int i = 1; i <= maxOfRoll; ++i)
        {
            rockRoll(sumOfRoll, currentRoll + 1, i + sum, allSums);
        }
    }
}

void printPossibilities(int sumOfRoll)
{
    if (sumOfRoll < 1)
        return;
    int* timesOfSumOfRoll = new int[maxOfRoll * sumOfRoll - sumOfRoll + 1];
    for (int i = 0; i < (maxOfRoll * sumOfRoll - sumOfRoll + 1); ++i)
        timesOfSumOfRoll[i] = 0;
    int total = pow((double)maxOfRoll, (double)sumOfRoll);
    rockRoll(sumOfRoll, 1, 0, timesOfSumOfRoll);
    for (int i = 0; i < maxOfRoll * sumOfRoll - sumOfRoll + 1; ++i)
    {
        double ratio = (double)timesOfSumOfRoll[i] / total;
        printf("%d : %d : %lf\n", i + sumOfRoll, timesOfSumOfRoll[i], ratio);
    }
    delete[] timesOfSumOfRoll;
}


void printPossibilities_good(int sumOfRoll)
{
    if (sumOfRoll < 1)
        return;
    int total = pow((double)maxOfRoll, (double)sumOfRoll);
    //下标表示和,值表示次数
    //用于轮流存储骰子第n个骰子的各种和出现的次数
    int* tables[2];
    tables[0] = new int[sumOfRoll * maxOfRoll + 1];
    tables[1] = new int[sumOfRoll * maxOfRoll + 1];
    //数组清空
    for (int i = 0; i < sumOfRoll * maxOfRoll + 1; i++)
    {
        tables[0][i] = 0;
        tables[1][i] = 0;
    }
    //轮换着使用
    int flag = 0;
    //投一个骰子
    for (int i = 1; i <= maxOfRoll; ++i)
        tables[flag][i] = 1;
    //投下一个骰子
    for (int next = 2; next <= sumOfRoll; ++next)
    {
        //清空即将使用的数组
        for (int i = 0; i < next; ++i)
            tables[1 - flag][i] = 0;
        //求解投下当前骰子出现各种总和的次数
        //可能的次数是1-6乘以next个骰子
        for (int sum = next; sum <= maxOfRoll * next; ++sum)
        {
            tables[1 - flag][sum] = 0;
            //minus表示本轮可能出现的点数
            //期待总数减去本轮点数,就是上一轮点数出现的次数
            for (int minus = 1; minus < sum && minus <= maxOfRoll; ++minus)
            {
                tables[1 - flag][sum] += tables[flag][sum - minus];
                int lastIndex = sum - minus;
            }
                

        }
        //切换数组
        flag = 1 - flag;
    }
    for (int sum = sumOfRoll; sum <= maxOfRoll * sumOfRoll; ++sum)
    {
        double ratio = (double)tables[flag][sum] / total;
        printf("%d : %d : %lf\n", sum, tables[flag][sum], ratio);
    }
    delete[] tables[0];
    delete[] tables[1];
}

int main()
{
    //printPossibilities(11);
    printPossibilities_good(15);
    return 0;
}

动态规划

假设f(m,s)表示投第m个骰子时,点数之和s出现的次数,投第m个骰子时的点数之和只与投第m-1个骰子时有关。

递归方程:f(m,s)=f(m-1,s-1)+f(m-1,s-2)+f(m-1,s-3)+f(m-1,s-4)+f(m-1,s-5)+f(m-1,s-6),表示本轮点数和为s出现次数等于上一轮点数和为s-1,s-2,s-3,s-4,s-5,s-6出现的次数之和。

初始条件:第一轮的f(1),f(2),f(3),f(4),f(5),f(6)均等于1.

需要求的是:f(n , n)、 f(n, n+1)…f(n, 6*n)

事前记得清空两个数组

61扑克牌的顺子

随机抽5张牌判断是否为顺子,大小王定义为0

2~10 A1 J11 Q12 K13,大小王充当任意数

方法:

对数组排序

统计0的个数

统计空缺总数

0个数大于空缺数则顺子

如果非0数字重复出现,一定不是顺子

// offer61扑克牌的顺子.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 

int compareInt(const void* a, const void* b)
{
    return *(int*)a - *(int*)b;
}

bool isContinuous(int* numbers, int length)
{
    if (numbers == nullptr || length < 1)
        return false;
    qsort(numbers, length, sizeof(int), compareInt);
    int numberOfZero = 0;
    int numberOfGap = 0;
    for (int i = 0; i < length && numbers[i] == 0; ++i)
        numberOfZero++;
    for (int i = 1; i < length; ++i)
    {
        int diff = numbers[i] - numbers[i - 1] - 1;
        if (diff == 0 && numbers[i] != 0)
            return false;
        numberOfGap += diff;
    }
    return (numberOfZero - numberOfGap) >= 0;
}

int main()
{
    std::cout << "Hello World!\n";
}

62约瑟夫环

方法1,链表环

使用list模拟链表环,因为list不是环形的,所以iterator到达end时候要置为begin

时间是O(nm) 空间是O(n)

方法2

f(n, m) = 0 n = 1

f(n, m) = [ f(n-1, m) + m ] % n n > 1

// offer62约瑟夫环.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
using namespace std;

int jose(int n, int m)
{
    if (n < 1 || m < 1)
        return -1;
    list circle;
    for (int i = 0; i < n; ++i)
    {
        circle.push_back(i);
    }
    auto iter = circle.begin();
    while (circle.size() > 1)
    {
        for (int i = 0; i < m - 1; ++i)
        {
            iter++;
            if (iter == circle.end())
                iter = circle.begin();
        }
        auto next = ++iter;
        if (next == circle.end())
            next = circle.begin();
        circle.erase(--iter);
        iter = next;
    }
    return *iter;
}

int josen(int n, int m)
{
    if (n < 1 || m < 1)
        return -1;
    int last = 0;
    for (int i = 2; i <= n; ++i)
        last = (last + m) % i;
    return last;
}

int main()
{
    std::cout << josen(5, 3);
}

63股票的最大利润

记录前面的最小值,对当前的数字进行卖出计算价值,记录最大价值

64求解1+++n,不能使用循环、判断语句

  1. 类的静态变量,再创建n个类
  2. 虚函数
#include 
using namespace std;
class A
{
public:
    A()
    {
        count++;
        sum += count;
    }
    int getSum()const{ return sum;}
private:
    static int count;
    static int sum;
};
int A::count = 0;
int A::sum = 0;

class B;
B* Array[2];
class B
{
public:
    virtual int getSum(int n)const{ return 0;}
};

class C : public B
{
public:
    virtual int getSum(int n) { return Array[!!n]->getSum(n - 1) + n;}
};

int main()
{
    A a[9];
    std::cout << a[0].getSum() << endl;
    B b;
    C c;
    Array[0] = &b;
    Array[1] = &c;
    std::cout << Array[1]->getSum(9) << endl;
}

65不用加减乘除做加法

使用位运算实现A+B

  1. A和B各位相加不计进位,即使用异或^,得到C
  2. A和B各位相加计算进位,即使用与&&,然后左移1位得到D
  3. C和D重复12过程,知道第2步的进位为零

65不用新变量交换a b的值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-er2hekY3-1586255098449)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1585403850641.png)]

66构建乘积数组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TdMRhjfz-1586255098450)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1585405374613.png)]

1585405413154

把乘积分程两部分

Bi= A0*A1*...*Ai-1*Ai+1*...An-1;
Bi=C*D;
C=A0*A1*...*Ai-1;
D=Ai+1*...An-1;

B 可以先从小到大循环全部乘以C,然后再从大到小循环乘以D

时间复杂位O(N)

//需要引用传参
void constructMultipyArray(vector &A, vector &B)
{
    if (A.size() == B.size() && A.size() > 1)
    {
        B[0] = 1;
        for (int i = 1; i < B.size(); ++i)
        {
            B[i] = B[i - 1] * A[i - 1];
        }
        double temp = 1.0;
        for (int i = B.size() - 2; i >= 0; --i)
        {
            //B[i] = B[i + 1] * A[i + 1];//直接这样做会把上一步的乘积轮番带过来,错误
            //下面是正确做法
            temp *= A[i + 1];
            B[i] *= temp;
        }
    }
}

67把字符串转为整数

整数的知识

  1. -128的补码表示是1000 0000,这个是特殊规定,-128的数值原码可以推导出来,但是从补码不能推导出-128
  2. 0的补码表示是00000000,其实本来10000000也可以表示-0,但是为了不混淆0的单一表示方式,所以给了-128
  3. 数值常数如 0x80000000,是一个无符号数
  4. 在long long中,补码0x80000000表示最小负数,补码0x7fffffff表示最大正数

字符串转整数要点

  1. 检查nullptr和”“空字符串

  2. 检查是否有除0~9之外的数

  3. 检查±号,使用bool型记录下来

  4. 数值位要用unsigned long long,这样可以通过检查是否overflow

    if ((isMinus && number > 0x80000000) ||
                (!isMinus && number > 0x7fffffff))
    
  5. 如果有不符合规定的数,统统返回0,置全局变量为false;

// offer67把字符串转为整数.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;
bool g_str2Int_result = false;
long long str2Int(const char* str)
{
    g_str2Int_result = true;
    if (str == nullptr || *str == '\0')
    {
        g_str2Int_result = false;
        return 0;
    }
    const char* p = str;
    bool isMinus = false;
    if (*p == '-' || *p == '+')
    {
        if (*p == '-')
            isMinus = true;
        p++;
    }
    unsigned long long number = 0;
    while (*p != '\0')
    {
        if (*p < '0' || *p > '9')
        {
            g_str2Int_result = false;
            return 0;
        }
        number *= 10;
        number += *p - '0';
        if ((isMinus && number > 0x80000000) ||
            (!isMinus && number > 0x7fffffff))
        {
            g_str2Int_result = false;
            return 0;
        }
        p++;
    }
    if (isMinus)
        number *= -1;
    return number;
}

int main()
{
    char str[20] = { 0 };
    cin >> str;
    std::cout << str2Int(str);
}

68两个树节点的最低公共祖先

二叉搜索树

二叉搜索树经过排序,左子树节点比父节点小,右子树节点比父节点大。

从根节点开始和两个节点进行比较,如果比两个节点大,说明两个节点都在根节点的左子树,把查找节点设为根节点的左子节点

如果比两个节点小,说明两个节点都在根节点的右子树,把查找节点设为根节点的右子节点

重复以上过程,知道找到第一个在两个节点数值之间的节点

有父节点指针的任意树

转化为求两个链表的第一个公共节点

没有父节点指针的任意树

方案1:

从根节点遍历,每遍历一个节点,判断两个节点是否在它的子树

直到遍历找到该节点的子树存在两个节点,但该节点的任何子节点的子树都不能满足两个节点均在该子树,那么该节点是最低公共父节点

方案2:

用两个链表保存从根节点导两个节点的路径,然后转化为求两个链表(从根到叶的顺序)的最后公共节点

链表值存放树节点的指针

1.通过DFS查找到两条路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YFgUB5ZP-1586255098451)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1585415271232.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p8pQb2p6-1586255098452)(C:\Users\cqlia\AppData\Roaming\Typora\typora-user-images\1585415259731.png)]

ator到达end时候要置为begin

时间是O(nm) 空间是O(n)

方法2

f(n, m) = 0 n = 1

f(n, m) = [ f(n-1, m) + m ] % n n > 1

// offer62约瑟夫环.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
#include 
using namespace std;

int jose(int n, int m)
{
    if (n < 1 || m < 1)
        return -1;
    list circle;
    for (int i = 0; i < n; ++i)
    {
        circle.push_back(i);
    }
    auto iter = circle.begin();
    while (circle.size() > 1)
    {
        for (int i = 0; i < m - 1; ++i)
        {
            iter++;
            if (iter == circle.end())
                iter = circle.begin();
        }
        auto next = ++iter;
        if (next == circle.end())
            next = circle.begin();
        circle.erase(--iter);
        iter = next;
    }
    return *iter;
}

int josen(int n, int m)
{
    if (n < 1 || m < 1)
        return -1;
    int last = 0;
    for (int i = 2; i <= n; ++i)
        last = (last + m) % i;
    return last;
}

int main()
{
    std::cout << josen(5, 3);
}

63股票的最大利润

记录前面的最小值,对当前的数字进行卖出计算价值,记录最大价值

64求解1+++n,不能使用循环、判断语句

  1. 类的静态变量,再创建n个类
  2. 虚函数
#include 
using namespace std;
class A
{
public:
    A()
    {
        count++;
        sum += count;
    }
    int getSum()const{ return sum;}
private:
    static int count;
    static int sum;
};
int A::count = 0;
int A::sum = 0;

class B;
B* Array[2];
class B
{
public:
    virtual int getSum(int n)const{ return 0;}
};

class C : public B
{
public:
    virtual int getSum(int n) { return Array[!!n]->getSum(n - 1) + n;}
};

int main()
{
    A a[9];
    std::cout << a[0].getSum() << endl;
    B b;
    C c;
    Array[0] = &b;
    Array[1] = &c;
    std::cout << Array[1]->getSum(9) << endl;
}

65不用加减乘除做加法

使用位运算实现A+B

  1. A和B各位相加不计进位,即使用异或^,得到C
  2. A和B各位相加计算进位,即使用与&&,然后左移1位得到D
  3. C和D重复12过程,知道第2步的进位为零

65不用新变量交换a b的值

[外链图片转存中…(img-er2hekY3-1586255098449)]

66构建乘积数组

[外链图片转存中…(img-TdMRhjfz-1586255098450)]

1585405413154

把乘积分程两部分

Bi= A0*A1*...*Ai-1*Ai+1*...An-1;
Bi=C*D;
C=A0*A1*...*Ai-1;
D=Ai+1*...An-1;

B 可以先从小到大循环全部乘以C,然后再从大到小循环乘以D

时间复杂位O(N)

//需要引用传参
void constructMultipyArray(vector &A, vector &B)
{
    if (A.size() == B.size() && A.size() > 1)
    {
        B[0] = 1;
        for (int i = 1; i < B.size(); ++i)
        {
            B[i] = B[i - 1] * A[i - 1];
        }
        double temp = 1.0;
        for (int i = B.size() - 2; i >= 0; --i)
        {
            //B[i] = B[i + 1] * A[i + 1];//直接这样做会把上一步的乘积轮番带过来,错误
            //下面是正确做法
            temp *= A[i + 1];
            B[i] *= temp;
        }
    }
}

67把字符串转为整数

整数的知识

  1. -128的补码表示是1000 0000,这个是特殊规定,-128的数值原码可以推导出来,但是从补码不能推导出-128
  2. 0的补码表示是00000000,其实本来10000000也可以表示-0,但是为了不混淆0的单一表示方式,所以给了-128
  3. 数值常数如 0x80000000,是一个无符号数
  4. 在long long中,补码0x80000000表示最小负数,补码0x7fffffff表示最大正数

字符串转整数要点

  1. 检查nullptr和”“空字符串

  2. 检查是否有除0~9之外的数

  3. 检查±号,使用bool型记录下来

  4. 数值位要用unsigned long long,这样可以通过检查是否overflow

    if ((isMinus && number > 0x80000000) ||
                (!isMinus && number > 0x7fffffff))
    
  5. 如果有不符合规定的数,统统返回0,置全局变量为false;

// offer67把字符串转为整数.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include 
using namespace std;
bool g_str2Int_result = false;
long long str2Int(const char* str)
{
    g_str2Int_result = true;
    if (str == nullptr || *str == '\0')
    {
        g_str2Int_result = false;
        return 0;
    }
    const char* p = str;
    bool isMinus = false;
    if (*p == '-' || *p == '+')
    {
        if (*p == '-')
            isMinus = true;
        p++;
    }
    unsigned long long number = 0;
    while (*p != '\0')
    {
        if (*p < '0' || *p > '9')
        {
            g_str2Int_result = false;
            return 0;
        }
        number *= 10;
        number += *p - '0';
        if ((isMinus && number > 0x80000000) ||
            (!isMinus && number > 0x7fffffff))
        {
            g_str2Int_result = false;
            return 0;
        }
        p++;
    }
    if (isMinus)
        number *= -1;
    return number;
}

int main()
{
    char str[20] = { 0 };
    cin >> str;
    std::cout << str2Int(str);
}

68两个树节点的最低公共祖先

二叉搜索树

二叉搜索树经过排序,左子树节点比父节点小,右子树节点比父节点大。

从根节点开始和两个节点进行比较,如果比两个节点大,说明两个节点都在根节点的左子树,把查找节点设为根节点的左子节点

如果比两个节点小,说明两个节点都在根节点的右子树,把查找节点设为根节点的右子节点

重复以上过程,知道找到第一个在两个节点数值之间的节点

有父节点指针的任意树

转化为求两个链表的第一个公共节点

没有父节点指针的任意树

方案1:

从根节点遍历,每遍历一个节点,判断两个节点是否在它的子树

直到遍历找到该节点的子树存在两个节点,但该节点的任何子节点的子树都不能满足两个节点均在该子树,那么该节点是最低公共父节点

方案2:

用两个链表保存从根节点导两个节点的路径,然后转化为求两个链表(从根到叶的顺序)的最后公共节点

链表值存放树节点的指针

1.通过DFS查找到两条路径

[外链图片转存中…(img-YFgUB5ZP-1586255098451)]

[外链图片转存中…(img-p8pQb2p6-1586255098452)]

你可能感兴趣的:(校招复习,数据结构&算法)