【剑指offer】剑指offer 练习笔记

剑指offer练习

写一个程序要注意:

1. 功能性测试(完成基本功能)

2. 边界值测试(单独处理边界)

3. 负面测试(处理无效数据)

其余的还有:

1. 变量命名规范化,可读化 

2. 释放一个内存时,要记得把引用这块内存的指针置为NULL

通知函数的调用者函数出错有三种方式,比如第16题涉及到如果对0求负数幂,由于不能对0求倒数,应该提示调用出错:

1. 返回值

2. 全局变量

3. 抛出异常

基本概念:

1. 鲁棒性(健壮性)指程序能够判断输入是否合乎规范要求,并对不符合要求的输入予以合理的处理。

1. 赋值运算符

class CMyString
{
char* m_pData; 
int size; 
}
写一个CMyString类的赋值运算符。
CMyString& operator = (const CMyString& s)
{
if( this == &s) return * this; 
if(m_pData) 
delete[] m_pDate;  // 或者delete m_pData; 也可以
m_pData = nullptr; 

m_pData = new char[s.size + 1]; 
strcpy(m_pData, s.m_pData); 
size = s.size; 
return * this;  
}
这种写法在内存不足无法分配时会抛出new char异常,m_pData成为一个空指针。改进的方法是先分配内存,成功之后再修改自身的数据,否则不修改。
CMyString& operator = (const CMyString& s)
{
    if(this == &s) return this; 
    char* temp = new char[s.size + 1]; 
    if(temp){ // 如果分配成功了
        delete m_pData; 
        m_pData = temp; 
        size = s.size; 
    }
    return *this;
}
另一种方法是使用一个临时对象,我们尽量不要在构造函数里面删除自身数据,而应该统一在析构函数里完成,通过临时对象可以完成这一点:
CMyString& operator = (const CMyString& s)
{
    if(this != &s)
    {
        CMyString temp = CMyString(s); 
        // 交换自身的数据指针和临时对象的数据指针
        char* tp = s.m_pData; 
        s.m_pData = m_pData; 
        m_pData = tp; 
    } // 出了if块之后,会调用temp的析构,释放它的内存
    
    return *this; 
}

2. 单例模式

单例模式有懒汉和饿汉两种,其中饿汉方式是线程安全的,一旦类被加载就会去创建实例,而且只会创建一次,所以在饿汉模式下的单利模式getInstance()里不需要判断static对象是否为空,是必非空的。
而懒汉模式在getInstance里实例化static对象,会发生线程安全性问题。如果两个线程先后实例化(都执行构造函数),会出现构造了两次的问题。懒汉模式的getInstance在多线程环境下需要加锁,否则就是线程不安全的。
懒汉模式
      | --  没加锁的懒汉模式(线程不安全)
      | -- 加锁的懒汉模式
             | -- 单次判断(线程安全但效率低)
             | -- 双判空(线程安全且高效)
2.1 加一个锁,一次判断。每次只有一个线程进入临界区,每次调用getInstance必须排队。
CMyClass* getInstance()
{
lock(); 
if(m_instance == NULL)
{
m_instance = new CMyClass();
}
unlock(); 
return m_instance; 
}
2.2 加一个锁,两次判断。因为只在第一次实例化的时候会有多线程问题,所以只需要在if判断快里加锁就可以了。
CMyClass* getInstance() 
{
if(m_instance == NULL) 
{
lock(); 
if(m_instance == NULL)  // 第二次判断是防止排队的第二个线程在第一个线程已经实例化之后,再次实例化
{
m_instance = new CMyClass(); 
}
unlock(); 
}
}

3. 找数组中重复的数字

问题描述:长度为n的数组里,数字大小在 0~ n-1之间,其中某些数字重复,但不知道哪些重复了,也不知道重复了几次。请找出任意一个重复的数字。
解法:
1. 给数组先排序,O(nlogn),然后以O(n)扫描一下,有相邻两个一样就找到了重复的。
2. 用哈希表,只需要一次遍历,时间复杂度O(n), 空间复杂度O(n),用来保存一个哈希表。
#include <iostream>
using namespace std;

#define SIZE 10
int hashTable[SIZE] = {0};

int main()
{
int num[SIZE] = {1, 2, 2, 3, 4, 5, 5 ,6, 7};
int repNum = -1;
for (int i = 0; i < SIZE; i++)
{
if (hashTable[num[i]] != 0)
{
repNum = num[i];
break;
}
hashTable[num[i]] ++;
}
cout << repNum;
}
3. 看题,容量为n的数组,元素内容也在0~n-1之间,说明如果元素无重复的话,应该是每个位置的索引和元素值一样,也就是说,数字i在排序之后应该出现在第i个位置上。而现在出现了重复,说明有个位置被占了。我们可以遍历数组,尝试把每个元素放到它理应在的位置上(数字i在第i个位置)。如果发现那个位置i被另一个i占了,说明i就是一个重复的数字。时间复杂度为O(n)。
#include <iostream>
using namespace std;

#define SIZE 10 

int main()
{
    int n[SIZE] = { 1, 2, 3, 4, 2, 5, 5 ,6, 7 };
#define swap(x, y)\
do {\
int t = x;\
    x = y;\
    y = t;\
}while(0)
    int res = -1; 
    for (int i = 0; i < SIZE; )
    {
        if (n[i] != i)
        {
            if (n[n[i]] == n[i])
            {
                res = n[i]; 
                break; 
            }
            // 把数字放到它应在的位置
            int v = n[i];
            // 注意这里不能n[n[i]],一旦n[i]改了,结果就不对了
            swap(n[i], n[v]);
        }
    }
    cout << res;
}

4. 二维数组中的查找

问题描述:有一个二维数组,每行都按照从左到右递增,每一列按照从上到下递增。写一个函数,接受一个二维数组和一个整数,判断二维数组中是否有该整数。

比如有一个二维数组: 

1 2 8 9 

2 4 9 12

4 7 10 13 

6 8 11 15

查找7, 5...

解决方案:从右上角(比如9)开始缩小查找区域,如果右上角的值大于目标值,则该列不可能有目标值,剔除一列,接着判断左边一列。如果某列第一个值小于目标值,则目标值有可能在此列,且不可能在此行,剔除此行(因为更左边更不可能有了,而右边的已经剔除了),规模立马缩小了。接着就是重复这个步骤,知道找到目标值,或者列到底,或者行到头,表示没有这个值。也就是说,每次我们只会去那右上角和目标值比较,不会去遍历整个表,或者某行某列。

#include <iostream>
using namespace std;
#define SIZE 4 
// 以(i, j)为右上角的矩形
bool find(int n[][SIZE], int num, int i, int j)
{
    if (i > SIZE || j < 0) // 递归终点
        return false; 
    if (n[i][j] > num)
        return find(n, num, i, j - 1);
    else if (n[i][j] == num)
        return true;
    else
        return find(n, num, i + 1, j); 
}

int main()
{
    int n[][SIZE] = { 
        {1, 2, 8, 9}, 
        {2, 4, 9, 12},
        {4, 7, 10, 13},
        {6, 8, 11, 15},
    };
    if (find(n, 14, 0, 3))
    {
        cout << "find number " << endl; 
    }
    else
    {
        cout << "didnt find number" << endl; 
    }
}

5. 替换空格

问题描述:实现一个函数,把字符串中的每个空格替换成"%20",例如,输入“We are happy.” ,则输出"We%20are%20happy."。比如给定一个string buffer,容量足够大,替换空格。
解决方案:我们可以先遍历一边字符O(n),统计字符串中空格的个数,以得到需要的字符串长度,新长度=原来长度+空格数x2。然后从新长度的末尾开始修改原buffer,从后往前复制是一个技巧,可以减少移动的次数,写法也很方便。
#include <iostream>
using namespace std;
#define SIZE 1024 // buffer size 
int main()
{
    char strbuf[SIZE] = "We are happy."; 
    //cout << sizeof(strbuf); // 1024
    int originalLen = strlen(strbuf) + 1; // 13 + 1, 包含结尾0
    int newlen = originalLen;
    for (int i = 0; strbuf[i] != 0; i++)
    {
        if (strbuf[i] == ' ')
            newlen += 2;
    }
    if (newlen > SIZE)
        return 0; 
    int pre = originalLen - 1; 
    int beh = newlen - 1; 
    while (pre >= 0)
    {
        if (strbuf[pre] == ' ')
        {// 替换
            strbuf[beh--] = '0';
            strbuf[beh--] = '2';
            strbuf[beh--] = '%';
        }
        else
        {// 直接覆盖
            strbuf[beh--] = strbuf[pre];
        }
        --pre; 
    }


    cout << strbuf;
    return 0;
}

6. 从尾到头打印链表

问题描述:链表是面试中常考的内容,链表的各种操作都要能默写出来,并且要保证robust。要从尾到头打印链表,也就是先打印本节点的下一个节点,再打印本节点。很简单的递归。
解决方案:
void PrintListReversingly(ListNode* root)
{
    if(root == NULL) return ;
    PrintListReversingly(root->m_pNext); 
    cout << root->m_pData << endl; 
}
递归都可以通过栈来改写成非递归:
void PrintListReversingly(ListNode* root)
{
    if(root == NULL) return ; 
    stack<ListNode*> nodes; 
    while(root)
    {
        nodes.push(root); 
        root = root->next; 
    }
    while(!nodes.empty())
    {
        ListNode* p = nodes.top();
        nodes.pop(); 
        cout << p->m_pData; 
    }
}

7. 重建二叉树

问题描述:二叉树也是面试中常考的内容,二叉树的各种操作也需要清楚。典型的平凡二叉树,结点的结构是: 
struct Node
{
Node* left; 
Node* right; 
int value; 
}
重建二叉树是指,给定了前序遍历和中序遍历结果,构造出一颗二叉树。并能给出它的后序遍历结果。比如给定了前序结果1, 2 , 4, 7, 3, 5, 6, 8, 和中序结果4, 7, 2, 1, 5, 3, 8, 6,要求用上面的Node结点结构重建二叉树,并返回根结点。
解决方案:看书的第63页的图。
#include <iostream>
using namespace std;
struct Node
{
    int value; 
    Node* right;
    Node* left; 
};
//
Node* rebuild(int dlr[], int dlr_p, int s,// 前序遍历和区间, dir_p表示前序遍历中的子序列索引,s表示中序和前序序列的大小
    int ldr[], int ldr_l, int ldr_r)    // 中序遍历和区间, ldr_l,ldr_r分别表示中序遍历中子树的范围
{
    // 边界
    if (dlr_p < 0 || dlr_p >= s) return NULL; 
    if (ldr_l > ldr_r) return NULL;
    // 判断
    Node* node = new Node; 
    node->value  = dlr[dlr_p]; // 前序遍历的第一个点就是根结点
    // 从中序遍历中寻找根结点的位置
    int pos = -1;
    for (int i = ldr_l; i <= ldr_r; i++)
    {
        if (ldr[i] == node->value)
        {
            pos = i;
            break; 
        }
    }
    int lsize = pos - ldr_l; // 左子树宽度
    int rsize = ldr_r - pos; // 右子树宽度
    node->left = rebuild(dlr, dlr_p + 1, s, ldr, ldr_l, pos - 1);
    node->right = rebuild(dlr, dlr_p + lsize + 1, s, ldr, pos + 1, ldr_r);
    return node;
}
// 后续遍历
void lrd(Node* root)
{
    if (root == NULL)return; 
    lrd(root->left); 
    lrd(root->right);
    cout << root->value << endl; 
}
int main()
{
    const int size = 8;
    int dlr[size] = {1, 2, 4, 7, 3, 5, 6, 8};
    int ldr[size] = {4, 7, 2, 1, 5, 3, 8, 6};
    Node* root = rebuild(dlr, 0, size, ldr, 0, size - 1);
    lrd(root);
}

8. 二叉树的下一个节点

问题描述:给定一颗二叉树和其中一个结点,如何找出中序遍历的下一个节点?树的节点中,除了左右指针,还有一个指向父结点的指针。
解决方案:
1. 如果这个节点是父结点的左节点,则下一个节点就是父结点
2. 如果这个节点是父结点的右节点,且有右节点,则下一个结点就是它的右子树的最左节点
3. 如果这个节点是父结点的右节点,且无右节点,则下一个结点就以它的父结点为基准的下一个节点,回到问题1.2,直到找到这样一个节点,满足1或2
Node* findNext(Node* node) 
{
    if(node == NULL) return NULL; 
    Node* parent = node->parent; 
    if(parent->left == node) return parent; 
    else 
    {// 是右节点
        Node* next = node->right ;
        if(next)
        {//,从右子树中找最左节点,一直遍历left,知道->left为空
            while(next->left)
                next = next->left;
        }
        else 
        {
            next = findNext(node->parent);
        }
        return next; 
    }
}   

9. 用两个栈实现一个队列、用两个队列实现一个栈

问题描述:这是一个典型的适配器模式,STL中的stack 和queue也是适配器,默认是由deque实现的。栈是一种只能在一端操作的数据结构,而且满足先进后出;队列是一种两端操作的结构,一端进,一端出(是不是很像动物 吐舌头),满足先进先出。
解决方案:为了方便,我直接用STL中的stack实现一个queue,queue相当于一个适配器。要实现的接口有:
void push(v_type value) ;
void pop(); 
v_type front();
bool empty();
这三个,我们使用两个stack来保存状态,队列中的元素,要么在stack1中,要么在stack2中。stack1用来保存push的对尾数据,stack2用来保存pop的队首数据。push时,始终往stack1push,无论它是不是空的,pop时:
1. 如果stack2为空,则把stack1中的所有元素pop出来,依次放入stack2,使得它们在stack2中的顺序和在stack1中的恰好相反,这样,先入队的酒会在stack2的顶上,然后pop 出来就好了。
2. 如果stack2非空,则直接pop出stack2顶部那个元素,肯定是最早进入队列的元素。
#include <iostream>
#include <stack>
using namespace std;
// MyQueue适配器
class MyQueue
{
public:
    MyQueue():s1(), s2()
    {
    }
    stack<int> s1; 
    stack<int>s2; 
    void push(int n)
    {
        s1.push(n);
    }
    void pop()
    {
        if (!s2.empty())
        {
            s2.pop(); 
            return;
        }
        while (!s1.empty())
        {
            int n = s1.top(); 
            s2.push(n);
            s1.pop();
        }
        s2.pop();
    }
    int front()
    {
        if (!s2.empty())
        {
            return s2.top();
        }
        while (!s1.empty())
        {
            int n = s1.top();
            s2.push(n);
            s1.pop();
        }
        return s2.top();
    }
    bool empty()
    {
        return s1.empty() && s2.empty(); 
    }
};
int main()
{
    MyQueue q;
    q.push(1);
    q.push(3);
    q.push(2);
    q.push(5);
    q.push(4);
    cout << q.front() << endl; 
    q.pop();
    cout << q.front() << endl; 
    while (!q.empty())
    {
        cout << q.front();
        q.pop();
    }
}
还有一个问题就是用两个队列实现一个栈,也是一个适配器,要实现的操作也还是:
void push(v_type value) ;
void pop(); 
v_type top();
bool empty();
这四个,由于栈有后入先出的特点,只能在一端操作。用两个队列实现一个栈的中心思想是 每次只有一个操纵队列(每次只有一个队列非空,书72页),而且在每次执行pop和top的时候另一个队列成为操纵队列。
void push(v_type value) ;
void pop(); 
v_type front();
bool empty();

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


class MyStack
{
public:
    MyStack() :q1(), q2()
    {
    }
    // 每次只会有一个队列非空
    queue<int> q1;
    queue<int> q2;
    void push(int n)
    {
        // 当前操纵队列
        queue<int>* cur = &q1;
        if (!q2.empty()) cur = &q2; 
        cur->push(n);
    }
    void pop()
    {
        if (empty()) return;
        queue<int>* cur = &q1; 
        queue<int>* another = &q2;
        if (!q2.empty())
        {
            cur = &q2;
            another = &q1; 
        }
        // 将前面的那些转移到另一个队列中
        while (cur->size() > 1)
        {
            int n = cur->front();
            another->push(n);
            cur->pop();
        }
        if(!cur->empty())
            cur->pop();
    }
    int top()
    {
        if (empty()) return 0;
        queue<int>& cur = q1;
        queue<int>& another = q2;
        if (!q2.empty())
        {
            another = q1;
            cur = q2;
        }
        // 将前面的那些转移到另一个队列中
        int n ;
        while (!cur.empty())
        {
            n = cur.front();
            another.push(n);
            cur.pop();
        }


        return n; 
    }
    bool empty()
    {
        return q1.empty() && q2.empty();
    }
};


int main()
{
    MyStack s; 
    s.push(1); 
    s.push(2);
    s.push(3);
    s.push(4);
    cout << s.top(); 
    s.pop(); 
    s.push(5);
    while (!s.empty())
    {
        cout << s.top(); 
        s.pop();
    }
}

10. 斐波那契数列

f(n) = > (
0, n = 0
1, n = 1 
f(n -1) +f(n - 2), n > 1
)
问题描述:输入n,求斐波那契数列的第n项。
解决方案:斐波那契数列的递归公式是 ,但是这样会有很多重复计算的问题(递归几乎都有)。可以通过一个备忘录来优化。或者直接从f(0)开始算起,逐步递推到f(n)
#include <iostream>
using namespace std;
int main()
{
    for (;;)
    {
        int n; 
        cin >> n;
        if (n == 1)
        {
            cout << 1 << endl;
            continue;
        }
        int fibMinusOne = 1; 
        int fibMinusTow = 0; 
        int fibN = 0; 
        for (int i = 2; i <= n; i++)
        {
            fibN = fibMinusTow + fibMinusOne;
            fibMinusTow = fibMinusOne;
            fibMinusOne = fibN;
        }
        cout << fibN << endl;
    }


    return 0;
}
10.2 青蛙跳台阶问题
有只青蛙一次可以跳1级台阶,2级台阶,或3级台阶。问跳上n级台阶有多少种跳法。
问题分析:这是一个斐波那契数列的应用。f(0) = 0 , f(1) = 1, f(2) = 2, f(3) = 3, f(4) = f(3) + f(2) + f(1),对于n阶台阶,第一步可以走3阶,2阶或1阶。所以f(n) = f(n - 3) +f(n-2) + f(n-1)。可以用递归+备忘录优化,也可以递推。略过。

11. 旋转数组的最小数字

问题描述:把一个数组最开始的若干个元素搬到数组的末尾,称为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组3,4,5,1,2为1,2,3,4,5的一个旋转,该旋转数组的最小值为1。
解决方案:采用二分法,充分利用旋转数组的特性,将旋转数组分为两部分。左子序列的第一个值一定大于等于右子序列的最后一个值。采用两个指针,一个从最左边开始,一个从最右边开始。每次检测 (l+r)/2位置上的值,将其与l和r上的比较,如果大于等于l上的值,一定在左子序列,最小值一定在此值右边;如果小于右等于r的值,一定在此值左边。如此就将问题缩小了一半。时间复杂度O(logn)。
#include <iostream>
using namespace std;
// l指针一定在左子序列
// r指针一定在右子序列
int find(int *n, int l, int r)
{
    if (n[l] < n[r]) return n[l]; // 处理 自身有序这种情况,可以直接返回
    if (r - l == 1) return n[r]; // 左右两指针相遇,只差一个,则最小值是右子序列的第一个数字
    int mid = (l + r) / 2.f;
    if (n[mid] >= n[l]) return find(n, mid, r); 
    if (n[mid] <= n[r]) return find(n, l, mid); 
}
int main()
{
    int n[] = {3, 4, 5, 1, 2};
    cout << find(n, 0, sizeof(n)/ sizeof(int) - 1);
}

12. 矩阵中的路径

问题描述:设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任何一格开始,每一步可以在矩阵中向左,右,上,下移动一格。如果一条路径经过了矩阵中的某一格,那么该路径不能再次进入该格子。例如,在下面的3x4矩阵中包含一条“bfce”的路径,但不包含"abfb"的路径,因为字符串的第一个字符b在占据了矩阵的第一行第二个格子之后,就不能再进入这个格子。
a b t g 
c  f c s
j  d e h 
解决方案:采用回溯法。详见书89页。很好理解,不过要注意的是对于“不能再进入这个格子”的处理,用一个同样维度的bool矩阵来标记。
#include <iostream>
using namespace std;
#define SIZE 4
// i, j 是当前点的位置
bool traceback(bool mask[][SIZE], char n[][SIZE], int r, int c, int i, int j, const char* s)
{
    if (i < 0 || i >= r || j < 0 || j >= c)
        return false;
    if (mask[i][j]) return false; // 不能重复访问一个节点
    if (n[i][j] == s[0])
    {// 找下一部分
        mask[i][j] = true; // 标记这个节点已经被访问过
        if (strlen(s) == 1) return true; // 已经相等
        if (traceback(mask, n, r, c, i, j - 1, s + 1) // 向左走
            || traceback(mask, n, r, c, i, j + 1, s + 1) // 向右走
            || traceback(mask, n, r, c, i - 1, j, s + 1) // 向上走
            || traceback(mask, n, r, c, i + 1, j, s + 1) // 向下走
            )
            return true;
        else
        {// 没有此路径
            mask[i][j] = false; 
            return false;
        }
    }
    else
        return false;
}
bool find(bool mask[][SIZE], char n[][SIZE], int r, int c, const char* s)
{
    for (int i = 0; i < r; i++)
    {
        for (int j = 0; j < c; j++)
        {
            if (n[i][j] == s[0])
                if (traceback(mask, n, r, c, i, j, s))
                    return true; 
        }
    }
    return false;
}
int main()
{
    char n[][SIZE] = {
        {'a', 'b', 't', 'g'},
        {'c', 'f', 'c', 's'},
        {'j', 'd', 'e', 'h' },
    };
    bool mask[][SIZE] = {
        {0},
        { 0 },
        { 0 },
    };
    const char* s = "bfce"; 
    //const char* s2 = "abfb";
    if (find(mask, n, 3, 4, s))
        cout << "the string is found" << endl;
    else
        cout << "can't find string" << endl; 
}

13. 机器人的运动范围

问题描述:地上有m行n列的方格,一个机器人从坐标(0,0)的格子开始移动,每次可以向左、右、上、下移动一格,但不能进入行坐标和列坐标的位数之和大于k的格子。例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7=18。但它不能进入方格(35, 38),因为3+5+3+8=19,请问机器人能够到达多少个格子?
解决方案:使用回溯法。
解决方案:
#include <iostream>
using namespace std;
int sum(int i, int j)
{
    int res = 0; 
    while (i > 0)
    {
        res += i % 10;
        i /= 10;
    }
    while (j > 0)
    {
        res += j % 10;
        j /= 10;
    }
    return res; 
}
void find(int m, int n,int i, int j, int k, int& count, bool* visited)
{
    if (i < 0 || i >= m || j < 0 || j >= n) return;
    if (visited[n * i + j]) return;
    if (sum(i, j) > k) return ; 
    if (!visited[n * i + j])
    {
        visited[n * i + j] = true; 
        count += 1;
    }
    find(m, n, i - 1, j, k, count, visited); // 向上走
    find(m, n, i + 1, j, k, count, visited); // 向下走
    find(m, n, i, j - 1, k, count, visited); // 向左走
    find(m, n, i, j + 1, k, count, visited); // 向右走
}
int main()
{
    for (;;)
    {
        int m, n, k; 
        m = 5;
        n = 5;
        k = 3; 
        int count = 0;
if(m <= 0 || n <= 0 || k < 0)
{
cout << count; 
continue; 
}
        bool* visited = new bool[m *n];
        for (int i = 0; i < m*n; i++)
            visited[i] = 0;
        find(m, n, 0, 0, k, count, visited);
        cout << count; 
    }
}

14. 剪绳子

问题描述:一根长度为n的绳子,把绳子剪成m段(m,n都是整数, n >1且m>1),每段绳子长度为k[0], k[1],... k[m],请问k[0]xk[1]x ... k[m]可能的最大乘积是多少?例如,当绳子长度是8时,我们把它剪成2,3,3,此时得到最大乘积18.注意,只有n是给定的,m是未给定的。
解决方法:用动态规划的方法。剪第一刀的时候有 n-1 种可能的选择,也就是剪出来的绳子的长度可能为1, 2, ... n-1。有递归公式 f(n) = max( f(i) * f(n - 1)), 0 < i < n 注意,第一剪剪下来的这一段也有自己的最优解,而不是简单的 i 。
#include <iostream>
using namespace std;
int main()
{
    int n = 8;
    int *product = new int[n + 1]; 
    // 边界
    product[0] = 0; 
    product[1] = 1; 
    product[2] = 2;
    product[3] = 3; 
    int max = 0;
    for (int i = 4; i <= n; i++)
    {
        max = 0;
        for (int j = 0; j <= i/2; j++)
        {
            int p = product[j] * product[i - j];
            if (p > max) max = p;
            product[i] = max;
        }
    }
    max = product[n]; 
    delete[] product; 
    cout << max; 
}
另一种解法是贪婪算法,当n>=5时,尽可能多剪成长度为3的绳子;当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。具体的证明和算法在97页。

15. 位运算

二进制的位运算有
与 &
或 | 
异或 ^
左移 <<
右移 >>
其中左右移的优先级最低,计算的时候经常要加括号。位运算的优先级总体来说比较低(比比较运算符低)。
15.1 微软Excel中,用A表示第一列,B表示第二列,。。。Z表示第26列,AA表示第27列,AB表示第28列...要求写一个函数,输入一个列号编码,输出它是第几列。
解决方案:是一个进制转换问题,把编码从26进制转换成十进制即可
#include <iostream>
#include <string>
#include <math.h>
using namespace std;
int f(const string s)
{
    int n = s.size(); 
    int res = 0; 
    for (int i = 0; i < s.size(); i++)
    {
        char c = s[i]; 
        int num = c - 'A' + 1; 
        res += num * pow((double) 26, (double)(--n));
    }
    return res; 
}
int main()
{
    for (;;)
    {
        string s; 
        cin >> s; 
        cout << f(s);
    }
}
15.2 统计二进制数中的1的个数
解决方案:
15.2.1 会在输入数字是负数时造成死循环(最高位是1)。
#include <iostream>
using namespace std; 
int countOne(int n)
{
    int res = 0; 
    while (n > 0)
    {
        res += (n & 1);
        n = n >> 1; 
    }
    return res; 
}
int main()
{
    for (;;)
    {
        int n; 
        cin >> n; 
        cout << countOne(n) << endl;
    }
}
15.2.2 为了避免死循环,可以不右移输入的数字n,可以把n与1做与运算,看最低位是不是1,然后把1左移一位得到2,在和n做与运算,就能判断n的次低位是不是1。。。反复左移1,每次都可以判断,无论是不是最高位的符号位。
#include <iostream>
using namespace std; 
int countOne(int n)
{
    int count = 0; 
    unsigned int flag = 1;
    while (flag)
    {
        if ((n & flag) != 0) count++; 
        flag = flag << 1; 
    }
    return count; 
}
int main()
{
    for (;;)
    {
        int n; 
        cin >> n; 
        cout << countOne(n) << endl;
    }
}
15.2.3 通过“把一个整数-1之后在和原来的数按位与,结果等于把整数的二进制表示中最右边的1变成0”。比如1100, 减一之后是1011, 再和原先的数按位与是1000, 这样就成功的只剔除了原先的最后一个1,循环往复,直到数为0即可。具体看书102
#include <iostream>
using namespace std; 
int countOne(int n)
{
    int count = 0; 
    while (n)
    {
        ++count;
        n = (n - 1) & n;
    }
    return count; 
}
int main()
{
    for (;;)
    {
        int n; 
        cin >> n; 
        cout << countOne(n) << endl;
    }
}

16. 数值的整数次方

问题描述:实现double Power(double base, int exponent), 不得使用库函数,同时不需要考虑大数问题。
解决方案:需要考虑以下问题:
1. 指数是负数
2. 不能对0求负幂,因为不能求0的倒数,应该报告调用出错
3. 0的0次方在数学上无意义,输出0或者1都可以
#include <iostream>
bool g_InvalidInput = false; // 保存错误信息
double PowerWithUnsignedExponent(double base, unsigned int exponent) // 计算正数幂
{
    double result = 1.0; 
    for (int i = 1; i <= exponent; ++i)
        result *= base;
    return result; 
}
double Power(double base, int exponent)
{
    g_InvalidInput = false;
    if (base == 0.0 && exponent < 0)
    {// 不能对0求负幂,因为不能对0求倒数
        g_InvalidInput = true; 
        return 0.0; 
    }
    unsigned int absExponent = (unsigned int)exponent;
    if (exponent < 0)
        absExponent = (unsigned int)(-exponent); 
    double result = PowerWithUnsignedExponent(base, exponent);
    if (exponent < 0)
        // 如果是负数幂,就取倒数
        result = 1.0 / result; 
    return result;
}
int main()
{
}
优化的方案是用下面的数列:
a^n = a^(n/2) * a^(n/2) , n为偶数
          a^((n-1)/2) * a^((n-1)/2) * a ,  n为奇数

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

问题描述:输入数字n,按顺序打印出从1到最大的n为十进制数字,比如输入3 ,打印1,2,3,一直到最大的三位数999。
解决方案:由于没有说n有多大,如果很大的话,即便用Longlong 保存也会超范围。解决方法是在字符串上模拟数字加法,因为字符串是没有长度限制的。
#include <iostream>
using namespace std;
// 给n +1
bool Increment(char* n)
{
    bool isOverflow = false;
    int nTakeOver = 0; // 进位
    int nLength = strlen(n); 
    for (int i = nLength - 1; i >= 0; i--)
    {
        int nSum = n[i] - '0' + nTakeOver; // 加上进位
        if (i == nLength - 1)
            nSum++; 
        if (nSum >= 10) // 需要进位
        {
            if (i == 0) // 如果已经到最高位了,即最后一个数字
                isOverflow = true; 
            else
            {
                nSum -= 10; 
                nTakeOver = 1; 
                n[i] = '0' + nSum; //刷新
            }
        }
        else
        {
            n[i] = '0' + nSum;
            break; 
        }
    }
    return isOverflow;
}
// 输出数字,注意别输出补位的零
void PrintNumber(const char* n)
{
    int len = strlen(n); 
    bool isBeginningZero = true; 
    for (int i = 0; i < len; i++)
    {
        if (n[i] == '0' && isBeginningZero)
        {
            continue; 
        }
        isBeginningZero = false;
        cout << n[i];
    }
    cout << '\n';
}
// 
void PrintToMaxOfNDigits(int n)
{
    if (n <= 0)
        return;
    char* number = new char[n + 1]; 
    memset(number, '0', n); 
    number[n] = '\0';// 此时 字符串应该是 0000...000\0, n个0, 结尾一个\0
    while (! Increment(number)) // 输出,直到位数已满
    {
        PrintNumber(number);
    }
    delete[] number; 
}
int main()
{
    for (;;)
    {
        int n; 
        cin >> n; 
        PrintToMaxOfNDigits(n);
    }
}
这样的话还是比较复杂的,在短时间内要写出来几乎不可能,可以用递归来实现更简单的方法。如果我们在所有数字前面补0,就会发现,n位左右十进制数其实就是n个从0到9的全排列。也就是说,只要把每一位从0到9全排列一遍,就得到了所有十进制数字。在打印的时候,忽略前导0就好了。
#include <iostream>
using namespace std;
void PrintToMaxOfNDigits(int n)
{
    if (n <= 0) return; 
    char* number = new char[n + 1]; 
    number[n] = '\0'; 
    for (int i = 0; i <= 9; i++)
    {
        number[0] = i + '0';
        PrintToMaxOfNDigitsRecursively(number, n, 0);
    }
}
// 输出数字,注意别输出补位的零
void PrintNumber(const char* n)
{
    int len = strlen(n);
    bool isBeginningZero = true;
    for (int i = 0; i < len; i++)
    {
        if (n[i] == '0' && isBeginningZero)
        {
            continue;
        }
        isBeginningZero = false;
        cout << n[i];
    }
    cout << '\n';
}
void PrintToMaxOfNDigitsRecursively(char* number, int length, int index)
{
    if (index == length - 1)
    {
        PrintNumber(number); 
    }
    for (int i = 0; i < 10; i++)
    {
        number[index + 1] = i + '0';
        PrintToMaxOfNDigitsRecursively(number, length, index + 1); 
    }
}
int main()
{
    for (;;)
    {
        int n; 
        cin >> n; 
        PrintToMaxOfNDigits(n);
    }
}

18. 删除链表的结点

问题描述:在O(1)时间内删除单链表的某个结点。给定一个表头指针和一个结点指针,要求定义一个函数,在O(1)时间内删除该结点。链表的数据结构如下:
struct ListNode
{
int m_pData; 
ListNode* m_pNext; 
}
解决方法:通常我们要删除单链表中一个结点,要从表头遍历找到此节点的上一个结点,然后进行一系列指针转移操作,时间复杂度是O(n)。此题要求时间复杂度O(1),那只能从此结点指针着手了,因为访问这个节点的时间是O(1)。可以用结点的下一个结点覆盖此结点,然后删除下一个结点即可。因为结点结构很简单,这样是可行的。
void RemoveNode(ListNode** head, ListNode* node) 
{
    if(*head == NULL || node == NULL) return ; 
    // 如果要删除的不是尾结点
    if(node->next != NULL) 
    {
        ListNode* next= node->next; 
        node->m_pDate = next->m_pData; 
        node->next = next->next; 
        delete next; 
        next = NULL;
    }
    // 如果只有一个结点,删除这个节点
    else if(*head == node) 
    {
        delete node; 
        node = NULL;
        *head= NULL:  
    }
    else 
    {// 链表有多个节点,且删除尾结点,则尾结点的上一个节点的next应该是0,
     // 这时候只能老老实实遍历了
        ListNode* n  = *head; 
        while(n->next != node)
        {
            n = n->next;
        }
        n->next = NULL; 
        delete node; 
        node = NULL; 
    }
}
18.2 删除已排序链表中重复的结点。比较简单,说一下思路。如果当前结点和下一个结点的值相同,那么就是重复的。

19. 正则表达式匹配

问题描述:实现一个函数,用来匹配包含'.'和'*'的正则表达式。模式中的字符 . 代表任意一个字符,而 * 代表它前面的字符可以出现任意多次(含0次)。匹配指该字符串的所有字符都匹配整个模式。例如,字符串“aaa”与模式"a.a", "ab*ac*a"匹配,但与"aa.a" . "ab*a"均不匹配。
解决方案:
#include <iostream>
using namespace std;
bool matchCore(char* str, char* pattern)
{
    // 递归终点
    if (*str == '\0'  && *pattern == '\0') // 当两者都走完时,匹配
        return true; 
    if (*str != '\0' && * pattern == '\0') // 当模式已经走完时,字符串还没走完,不匹配。(题目要求字符串中的所有字符匹配整个模式);翻过来的情况在下面的代码中判断,即*str = '\0’返回false。
        return false; 
    if (*(pattern + 1) == '*')
    {// 如果模式的第二个字符是* 或者模式允许任意字符出现任意多次即".*"模式
        if (*str == *pattern || *pattern == '.' && *str != '\0')
        {// 如果str的第一个字符和pattern的第一个匹配,则str跳过一个,或者pattern跳过这个*匹配(即匹配0次),或者两者都跳过
            return matchCore(str + 1, pattern)
                || matchCore(str, pattern + 2)
                || matchCore(str + 1, pattern + 2);
        }
        else
        { // 如果不匹配,则跳过这个* 匹配(因为*允许出现0次前面的字符)
            return matchCore(str, pattern + 2); 
        }
    }
    if (*str == *pattern || (*pattern == '.' && *str != '\0'))
        return matchCore(str + 1, pattern + 1);
}
bool match(char* str, char* pattern)
{
    if (str == NULL || pattern == NULL) return false;
    return matchCore(str, pattern);
}

20. 表示数值的字符串

跳过
问题描述:实现一个函数,判断字符串是否表示数值(包括整数、小数)比如,字符串"+100","5e2", "-123", "3.1416"以及"-1E-16"都表示数值,但"12e", "1a3.14",“1.2.3”,"+-5","12e+5.4"都不是数值。
解决方案:表示数值的字符串遵循模式 A[.[B]][e|EC]或者.B[e|EC], 其中A为数值的整数部分,B为小数部分,C为指数部分,C只能是整数,不会是小数。

21. 调整数组顺序使得奇数位于偶数前面

问题描述:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
解决方案:利用快速排序一样的思想,一个左指针,一个右指针,左指针碰到偶数, 右指针碰到奇数,就交换两者,知道两个指针相遇。
#include <iostream>
using namespace std;
int main()
{
    int n[] = {0, 1,2,3,4,5,6,7,8};
    int size = sizeof(n) / sizeof(int);
    int l = 0; 
    int r = size - 1; 
    while (l < r)
    {
        while ((n[r] & 1) == 0) --r; // 位运算符优先级低于比较运算符。位运算优先级一般很低,移位最低
        while ((n[l] & 1) == 1) ++l;
        if (l >= r) break;
        n[r] ^= n[l];
        n[l] ^= n[r]; 
        n[r] ^= n[l];
        l++;
        r--;
    }
    for (int i = 0; i < size; i++)
        cout << n[i] << " " << endl; 
}

22. 获得链表中倒数第k个节点

问题描述:输入一个链表,输出该链表的倒数第N个节点。从1开始计数,链尾是倒数第一个节点。
解决方案:两个指针,一个快指针,一个慢指针,快指针走n-1步,然后一起走,等块指针走到尾部,慢指针的位置就是倒数第n个结点
++++++------------------------------ 快指针先走N步,到了最后一个加号处
-----------------------------+++++++然后一起走,最后慢指针到了最左边那个加号处,两个指针走的构成镜像
证明:假设链长为L(未知),快指针走到正数第N个结点时(走了N-1步),剩余L-N个没走,需要走L-N步。此时两者一起走,等快指针到了链尾,慢指针走了L-N步,到了正数第L-N+1个,对应于倒数第N个(正数倒数的编号和是L+1)。
ListNode * RGetKthNode(ListNode * pHead, unsigned int k) // 函数名前面的R代表反向  
{
    if (k == 0 || pHead == NULL) // 这里k的计数是从1开始的,若k为0或链表为空返回NULL  
        return NULL;
    ListNode * pAhead = pHead;
    ListNode * pBehind = pHead;
    while (k > 1 && pAhead != NULL) // 前面的指针先走到正向第k个结点, 走了k-1步
    {
        k--;
        pAhead = pAhead->m_pNext;
    }
    if (k > 1 || pAhead == NULL)     // 结点个数小于k,返回NULL  
        return NULL;
    while (pAhead->m_pNext != NULL)  // 前后两个指针一起向前走,直到前面的指针指向最后一个结点  
    {
        pBehind = pBehind->m_pNext;
        pAhead = pAhead->m_pNext;
    }
    return pBehind;  // 后面的指针所指结点就是倒数第k个结点  
}

23. 链表中环的入口结点

略过,见另一篇有关链表的博客

24. 反转链表

略过,同上

25. 合并两个已排序的链表

基本思路是每次从两个链表头部选小的(如果是递增链表的话),然后把next指针指向这个结点。
略过,同上

26. 树的子结构

问题描述:输入两个二叉树A和B,判断B是不是A的子结构。
解决方案:
1. 现在A中查找与B根结点一样的值,等价于树的遍历。
2. 以匹配的根为根,递归的判断结构是否一致。
分为两个函数,一个用来找到那个根,另一个以那个根为根,递归的判断。
详见书150页。

27. 翻转二叉树

* 28. 对称的二叉树

问题描述:实现一个函数,用来判断一个二叉树是不是对称的(关于根结点对称),即左右翻转之后,还是它。
解决方案:我们定义一种遍历方法,类前序遍历,只不过现在是先遍历右子树,再遍历左子树。用两个指针,一个往左走,按照前序遍历,先左子树,再右子树;一个往右走,先右子树,再左子树。
bool isSymmetraical(BinaryNode* pRoot)
{
    return isSymmetraical(pRoot, pRoot);
}
bool isSymmetraical(BinaryNode* pRoot1, BinaryNode* pRoot2) // root1往左走,root2往右走
{
    if(pRoot1 == NULL && pRoot2 == NULL)
        return true; 
    if(pRoot1 == NULL || pRoot2 == NULL) // 有一个不为空
        return false;
    if(pRoot1->m_Value != pRoot2->m_Value)
        return false; 
    return isSymmetraical(pRoot1->m_left, pRoot2->m_right) 
    &&     isSymmetraical(pRoot1->m_right, pRoot2->m_left);
}

29. 顺时针打印矩阵

问题描述:输入一个矩阵,按照从外向内以顺时针打印出每一个数字。如
1 2 3 4 
5 6 7 8 
9 10 11 12
13 14 15 16 
依次打印出1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10
画图分析,可以把矩阵看成是好几圈构成的结构,每次打印外面的一圈,然后回到再里面一圈的左上角。

30. 包含min函数的栈

问题描述:定义栈的数据结构,在该类型中实现一个能得到栈最小值的min函数。在此栈中,调用min,push,pop的时间都是O(1)。
解决方案:用另一个辅助栈存贮最小值,不能用一个数字保存(因为如果那个最小值被弹出了,栈的最小是什么?)。
每次往栈中压入元素,检查此元素和辅助栈栈顶元素(最小值)的大小关系,如果比最小值小,则把此值压入辅助栈,否则(大于或等于),再压入一个最小值。
从栈中pop数据的时候,辅助栈也pop一个。这样就可以解决栈中有多个最小值的问题了。每次使用min获得最小值,一定是辅助栈栈顶元素。

31. 栈的压入、弹出序列

问题描述:定义两个序列,一个代表栈的入栈顺序,一个代表出栈顺序,判断出栈顺序是否能实现。假设压入的数字都不相等。 

解决方案:
以弹出序列为基准,参考入栈顺序,入栈和出栈:
如果下一个弹出的数字刚好是栈顶数字,则直接弹出;
如果下一个弹出的数字不是栈顶数字,则把压栈序列中还没有入栈的序列压入,直到从中找到了下一个弹出数字,压入栈;
如果所有数字都压入栈了,还没有从中(入栈序列中)找到下一个出栈数字,则该弹出序列不可能是一个弹出序列。

32. 从上到下打印二叉树(二叉树层次遍历/广度优先遍历)

依靠队列实现,略。

33. 二叉搜索树的后序遍历序列

问题描述:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。假设数组内容无重复。
解决方案:二叉搜索树相比平凡二叉树的特点就是
1. 中序有序性,即中序遍历是有序的。
2. 给定了前序遍历结果或者后序遍历结果,就可以确定BST的结构,而平凡二叉树不行。
比如5, 7, 6, 9, 11, 10, 8这个后序遍历,8一定是BST的根,8左边那些是左子树和右子树,由于二叉树中左子树中所有值均<根<右子树中所有值,所以在这些树中,很容易发现5,7,6是左子树,9,11,10是右子树,这些数字一定是连着的。然后左右子树又是BST,重复递归,知道叶节点。
注意如何判断假,比如 7, 4, 6, 5这个序列,5是根,从左往右遍历这个序列,第一个数字7>5,所以7在右子树,且此树应该没有左子树,那么接下来的值应该都在右子树,即7,4,6在右子树,但我们发现右子树中有一个节点4小于5,这违背了BST的定义,所以,这个序列不是一个可行的BST后序遍历序列。
代码参考书上181.

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

跳过
问题描述:输入一颗二叉树和一个整数,打印出二叉树节点值的和为输入整数的所有路径。必须到达叶节点。

35. 复杂链表的复制

跳过

36. 二叉搜索树转化为双向链表

看另一个博客
利用二叉搜索树的中序有序性。分类:

37. 序列化二叉树

写两个函数,分别实现二叉树的序列化和反序列化
解决方案:
1. 序列化为前序和中序结果,然后重建。缺点是不能有重复的节点
2. 采用前序遍历,因为前序遍历的第一个节点是根结点。遇到一个空指针,就保存为一个特殊字符比如$。尝试反序列化1,2,4,$,$,$,3,5,$,$,6,$,$时,1时根结点,第二个值2非$,说明1的左子结点是2(如果2是$的话,说明1的左子树为空)。
具体看书195

* 38. 字符串的排列

问题描述:输入一个字符串,打印该字符串的所有排列。
解决方案:不要去尝试排列组合,统计所有字符在排列。而是从现有字符串开始。交换两个字符就会获得一个新的字符串。
#include <cstdio>
void Permutation(char* pStr, char* pBegin);\
void Permutation(char* pStr)
{
    if(pStr == nullptr)
        return;
    Permutation(pStr, pStr);
}
void Permutation(char* pStr, char* pBegin)
{
    if(*pBegin == '\0')
    {
        printf("%s\n", pStr);
    }
    else
    {
        for(char* pCh = pBegin; *pCh != '\0'; ++ pCh)
        {
            char temp = *pCh;
            *pCh = *pBegin;
            *pBegin = temp;
            Permutation(pStr, pBegin + 1); // 处理后面的
            temp = *pCh;    
            *pCh = *pBegin;
            *pBegin = temp;
        }
    }
}
// ====================测试代码====================
void Test(char* pStr)
{
    if(pStr == nullptr)
        printf("Test for nullptr begins:\n");
    else
        printf("Test for %s begins:\n", pStr);
    Permutation(pStr);
    printf("\n");
}
int main(int argc, char* argv[])
{
    Test(nullptr);
    char string1[] = "";
    Test(string1);
    char string2[] = "a";
    Test(string2);
    char string3[] = "ab";
    Test(string3);
    char string4[] = "abc";
    Test(string4);
    return 0;
}

* 39. 数组中出现次数超过一半的数字

问题描述:数组中有一个数字的出现次数超过了数组长度的一半,找出这个数字。
解决方案:O(n)根据数组的特点。一个次数出现次数超过数组长度的一半,说明它出现的次数比其它数字出现的次数的和还要多。 要注意的是,题目已经保证了数组中必定有一个数字出现次数超过数组的一半。下面的算法只在这种情况下有效,
用两个值,一个保存数字,一个保存次数。当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加1; 如果下一个数字和我们之前保存的数字不同,则次数减一,此时如果次数为0,则保存下一个数字,并把次数设为1。由于我们要找的数字出现的次数很多,比其他数字出现次数加起来还要多,那么要找的那个数字,一定会活到最后(最后一次把次数设为1的那个)。

* 40. 最小的k个数(TOP K问题)

问题描述:输入n个整数,找出其中最小的k个数。例如,输入4,5,1,6,2,7,3,8,这8个数字,则最小的4个数字是1,2,3,4
解决方案:
1. 最简单的就是先拍好,然后选出前k个,复杂度是O(nlogn)
2. 基于partition(快排)的算法, 复杂度为O(n)
在使用快排过程中,记下来每次排序选择的
Quicksort相关原理就不多说了,STL中Quicksort算法的实现我在<stl sort源码剖析>中一文也详细说明过。所以这里就只说说怎么样用Quicksort来求topk。
其实很简单,大家都知道Quicksort每次分割后即把比轴小的值和比轴大的值给cut开了。那么,假如是想求前k小(以下源码即是),我就只关心比轴小的一边,继续分割这一边,直到如果分割后的元素没有k个了,就结束循环。我们用源码说话:
TopK问题即求序列中最大或最小的K个数。这里以求最小K个数为例。

快速排序的思想是使用一个基准元素将数组划分成两部分,左侧都比基准数小,右侧都比基准数大。

给定数组array[low…high],一趟快排划分后的结果有三种:

1)如果基准数左侧元素个数Q刚好是K-1,那么在基准数左侧(包含基准数本身),即为TopK的所有元素。

2)如果基准数左侧元素个数Q小于K-1,那么说明基准数左侧的Q个数都是TopK里的元素,只需要在基准数的右侧找出剩下的K-Q个元素即可。问题转化成了以基准数下标为起点,高位(high)为终点的Top(K-Q)。递归下去即可。

3)如果基准数左侧元素个数Q大于K-1,说明第K个位置,在基准数的左侧,需要缩小搜索范围,在低位(low)至基准数位置重复递归即可,最终问题会转化成上面两种情况。




























































  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值