剑指offer-第二版

【剑指offer-第二版】习题感悟(一刷)

1. 面试流程

一般分为三轮面试:2-3轮技术面,1轮HR面

每一轮技术面试分为3个环节:行为面试,技术面试,提问环节

  • 行为面试:自我介绍,按照简历上内容进行项目的提问;
  • 技术面试:编程语言,数据结构与算法等基础知识,手撕代码的能力等;
  • 提问环节:推荐提问和应聘职位相关的问题,或者项目团队氛围怎么样等等。

2. 面试的基础知识

2.1 C++基础知识

面试题1:赋值运算符函数
class CMyString
{
	private:
		char* m_pData;
	
	public:
		CMyString(const char* pData = nullptr);
		CMyString(const CMyString& str);
		~CMyString(void);

		CMyString& operator=(const CMyString& str)
		{
			if(this != &str)
			{
				// 调用拷贝构造函数,作用域之外直接析构释放内存
				CMyString strTemp(str);
				
				// 交换临时变量
				char* pTemp = strTemp.m_pData;
				strTemp.m_pData = m_pData;
				m_pData = pTemp;
			}
			return *this; 
		}
};


CMyString::CMyString(const char* pData)
{
	if(pData == nullptr)
	{
		m_pData = new char[1];
		m_pData ='\0';
	}
	else
	{
		m_pData = new char[strlen(pData)+1];
		strcpy_s(m_pData, strlen(pData)+1, pData);
	}
}
面试题2:实现单例模式

单例模式:设计一个类,只能生成该类的一个实例。

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

class CSingleton{

private:
	CSingleton(){}
	static CSingleton* m_pInstance;
	static mutex mux_;
	
public:
	static CSingleton *GetInstance()
	{
		if(m_pInstance == nullptr)
		{
			lock_guard<mutex> lock(mux_);
			m_pInatance = new CSingleton();
		}
		return m_pInstance;
	}
};

CSingleton* CSingleton::m_pInstance = nullptr;

2.2 数据结构

面试题3:数组中重复的数字

题目1:找出数组中重复的数字

// 解法1:使用hash表
bool duplicate(int number[], int length, int* duplication)
{
	if(number == nullptr || length <=0) 
		return false;
	
	int * hashTable = new int[length]();
	for(int i=0; i<length; i++)
	{
		if(hashTable[number[i]])
		{
			*duplication = number[i];
			return true;
		}
		else{
			hashTable[number[i]] = 1;
		}
	}
	delete[] hashTable;
	return false;
}

题目2:不修改数组找出重复的数字

// 哈希表求解
int duplicate(int number[], int length)
{
	if(number == nullptr || length<=0)
		return -1;
	
	int hashTable = new int[length]();
	for(int i=0; i<length; i++)
	{
		if(hashTable[number[i]])
			return i;
		else
			hashTable[number[i]] = 1;
	}
	delete[] hashTable;
	return -1;
}
面试题4:二维数组中查找
// 思路:按照从右上角查找比较,按照比较结果删除一列或者一行
bool findMatrix(int* matrix, int rows, int cols, int number)
{
	if(matrix != nullptr && cols>0 && rows>0)
	{
		int row = 0;
		int col = cols-1;
		while(row<rows && col>=0)
		{	
			if(matrix[row*cols+col] == number)
				return true;
			else if(matrix[row*cols+col] > number)
				col--;
			else
				row++;
		}
	}
	return false;
}
面试题5:替换字符串中的空格
// 思路:统计字符串的长度和空格个数,然后从后向前检索替换,注意临界条件
void ReplaceBlank(char string[], int length)
{
	if (string == nullptr || length <= 0)
		return;

	int origenLength = 0;
	int blankLength = 0;

	int i = 0;
	while (string[i] != '\0')
	{
		origenLength++;
		if (string[i] == ' ')
			blankLength++;
		++i;
	}

	int newLength = origenLength + blankLength * 2;

	if (newLength > length)
		return;

	while (newLength > origenLength || origenLength >= 0)
	{
		if (string[origenLength] == ' ')
		{
			string[newLength--] = '0';
			string[newLength--] = '2';
			string[newLength--] = '%';
		}
		else
			string[newLength--] = string[origenLength];

		origenLength--;
	}
}

// 使用string
string ReplaceBlank(string s)
{
	string ss;
	for(int i=0; i<s.size(); i++)
	{
		s[i]==' ' ? ss+="%20" : ss+=s[i];
	}
	return ss;
}
面试题6:从尾到头打印链表
// 思路:使用栈存储,实现先进后出
void ListReverse(ListNode* head)
{
	stack<ListNode*> stk;
	ListNode* node = head;
	while(node != nullptr)
	{
		stk.push(node);
		node = node->next;
	}
	while(!stk.empty())
	{
		cout<<stk.top()->value<<" ";
		stk.pop();
	}
}
面试题7:重建二叉树
// 根据前序遍历,中序遍历重建二叉树
// 思路:先根据前序遍历找到根结点,在利用根结点信息在中序遍历中查找根结点序号。最后递归嗲用构建左右子树
class TreeNode
{
public:

	int m_value;
	TreeNode* left;
	TreeNode* right;

	TreeNode(int x) : m_value(x), left(nullptr), right(nullptr) { }
};

TreeNode* BuildTree1(vector<int> preorder, vector<int> inorder)
{

	if (preorder.size() != inorder.size() || preorder.size() == 0)
		return nullptr;

	int root = preorder[0];

	TreeNode* node = new TreeNode(root);

	int root_idx = -1;
	for (int i = 0; i < inorder.size(); ++i)
	{
		if (inorder[i] == root)
		{
			root_idx = i;
			break;
		}
	}

	if (root_idx == -1)
		return nullptr;

	int leftsize = root_idx;
	int rightsize = inorder.size() - root_idx - 1;

	node->left = BuildTree(vector<int>(preorder.begin() + 1, preorder.begin() + leftsize + 1), vector<int>(inorder.begin(), inorder.begin() + leftsize));
	node->right = BuildTree(vector<int>(preorder.end() - rightsize, preorder.end()), vector<int>(inorder.end() - rightsize, inorder.end()));

	return node;

}
面试题8:二叉树的下一个节点
// 思路:可以分为两种情况
// 情况1:该节点存在右子树,则下一节点就是右子树的最左子节点
// 情况2:该节点不存在右子树,可根据该节点的父节点的左子结点是否与本身相同判断,然后确定下一个子节点

class TreeLinkNode
{
	int m_value;
	TreeLinkNode* left;
	TreeLinkNode* right;
	TreeLinkNode* parent;

	TreeLinkNode(int x) : m_value(x), left(nullptr), right(nullptr), parent(nullptr){}
};

TreeLinkNode* GetNextNode(TreeLinkNode* pNode)
{
	if(pNode == nullptr) return nullptr;

	TreeLinkNode* pNext = nullptr;
	if(pNode->righe != nullptr)
	{
		TreeLinkNode* pRight = pNode->right;
		whiel(pRight->left != nullptr)
			pRight = pRight->left;
		pNext = pRight;
	}
	else
	{
		TreeLinkNode* pParent = pNode;
		while(pParent->parent != nullptr)
		{
			if(pParent->parent->left == pParent)
			{
				pNext = pParent->parent;
				break;
			}
			pParent = pParent->parent;
		}
	}
	return pNext;
}
面试题9:两个栈实现队列or两个队列实现栈
// 思路:利用一个栈专门用于出队列,另一个栈用于入队列

class cQueue
{
public:
	cQueue() {}
	~cQueue() {}

	// 实现入队列
	void appendTail(const int& node)
	{
		stack1.push(node);
	}
	// 实现出队列
	int deleteHead()
	{
		//if(stack.empty() && stack2.empty())
			//return ;
		
		if(stack2.empty())
		{
			while(!stack1.empty())
			{
				stack2.push(stack1.top());
				stack1.pop();
			}
		}
		
		int n = stack2.top();
		stack2.pop();
		
		return n;
	}

private:
	stack<int> stack1;
	stack<int> stack2;
};

// 两个队列实现栈
// 思路:保证其中一个栈为空
class cStack
{
public:
	cStack() {}
	~cStack() {}

	void appendHead(const int& node)
	{
		if (!que1.empty())
		{
			que1.push(node);
		}
		else
		{
			que2.push(node);
		}
	}

	int deteleNode()
	{
		//if (que1.empty() && que2.empty())
			//return ;

		int node = 0;
		if (!que1.empty())
		{
			int num = que1.size();
			while (num > 1)
			{
				que2.push(que1.front());
				que1.pop();
				--num;
			}
			node = que1.front();
			que1.pop();
		}
		else
		{
			int num = que2.size();
			while (num > 1)
			{
				que1.push(que2.front());
				que2.pop();
				--num;
			}
			node = que2.front();
			que2.pop();
		}
		return node;
	}

private:
	queue<int> que1;
	queue<int> que2;
};

2.3 算法和数据操作

面试题10:斐波那契数列
// 思路:采用递归的方式调用效率低,原因在于存在重复的调用
// 变种,小青蛙跳台阶(根据跳的方式累加)
int Fn(int n)
{
	if(n<=1) return n;

	int result = 0;  // 保存结果 f(n)
	int pre = 0;  // f(n-2)
	int cur = 1;  // f(n-1)
	for(int i=2; i<=n; i++)
	{
		// 计算f(n-1)+f(n-2)的结果
		result = pre + cur;
		
		pre = cur; // 向下传递
		cur = result; // 向下传递
	}
	return result;
}

快速排序算法
//思路:首先找到系列中的基准(一般选第一个);然后找到起始和终止结点;之后从左到右和从右到左遍历。最后递归实现左右边的排序
void QuickSort(int* data, int start, int end)
{
	if(start < end)
	{
		int std = data[start];
		int low = start;
		int high = end;

		while(low<high)
		{
			while(low<high && data[high]>std)
				--high;
			data[low] = data[high];
			
			while(low<high && data[low]<=std)
				++low;
			data[high] = data[low];
		}
		data[low] = std;
		QuickSort(data, start, low-1);
		QuickSort(data, low+1, end);
	}
	else
		return ;
}
面试题11:旋转数组的最小数字
//思路:需要根据序列确定不同情况
// 1.如果序列中包含相同的数字,则需要找该序列的最小值
// 2. 如果序列式正常递增,则需要利用二分法查找

//查找最小数字
int minInorder(vector<int> vec)
{
	int temp = vec[0];
	for(auto& v:vec)
	{
		if(v<temp)
			temp = v;
	}
	return temp;
}

int MinOrder(vector<int> rotateVec)
{
	int size = rotateVec.size();
	if(size == 0) return 0;
	if(size == 1 && rotareVec[0]<rotateVec[size-1]) return rotateVec[0];

	int low = 0;
	int high = size-1;
	while((high-low)>1)
	{
		int mid = (high+low)>>1;
		if(rotateVec[mid]==rotateVec[low] && rotateVec[mid]==rotateVec[high])
			return minInorder(rotateVec);
		if(rotateVec[mid] <= rotateVec[high])
			high = mid;
		else if(rotateVec[mid] >= rotateVec[low])
			low = mid;
	}
	return rotateVec[high];
}
面试题12:矩阵中的路径
// 思路:使用回溯的方法实现

// 1. 主题
bool hasPath(string matrix, int cols, int rows, string str)
{
	if(matrix.size() == 0 || cols<0 || row<0 || str.size() == 0)
		return false;
	
	bool* visited = new bool[rows*cols];
	memset(visited, 0, rows*cols);
	
	int pathLength = 0;
	for(int row=0; row<rows; row++)
	{
		for(int col=0; col<cols; col++)
		{
			// 调用回溯算法
			if(hasCore(matrix, cols, rows, col, row, str, pathLength, visited))
			{
				delete[] visited;
				return true;
			}
		}
	}
	delete[] visited;
	return false;		
}

// 2. 回溯法核心
bool hasCore(string matrix, int cols, int rows, int col, int row, string str, int& pathLength, bool* visited)
{
	if(str[pathLength] == '\0')
		return true;
	
	bool haspath = false;
	if(row>0 && col>0 && col<cols && row<rows && str[pathLength] == matrix[col+cols*row] && !visited[col+cols*row])
	{
		pathLength--;
		visited[col+cols*row] = true;
		haspath = hasCore(matrix, cols, rows, col-1, row, str, pathLength, visited)
			|| hasCore(matrix, cols, rows, col+1, row, str, pathLength, visited)
			|| hasCore(matrix, cols, rows, col, row-1, str, pathLength, visited)
			|| hasCore(matrix, cols, rows, col, row+1, str, pathLength, visited);
		if(!haspath)
		{
			pathLength--;
			visited[col+cols*row] = false;
		}
	}
	return haspath;
}
面试题13:机器人的运动范围
// 思路:保证机器人满足一下三个条件,表示可以运动。依旧使用回溯法
// 1. 机器人的下一个位置是边界处
// 2. 机器人已经走过该路径
// 3. 数位之和大于阈值
int getNumber(int data)
{
	int sum = 0;
	while(data!=0)
	{
		sum += data%10;
		data = data /10;
	}
	return sum;
}

int CountRobotCore(int threshold, int cols, int rows, int col, int row, bool* visited)
{
	if(threshold<(getNumber(col)+getNumber(row)) || col<0 || row<0 ||col>=cols || row>=rows || visited[col+cols*row])
		return 0;
	
	int res = 1;
	visited[col+cols*row] = true;
	res + = CountRobotCore(threshold, cols, rows, col-1, row, visited)
			+ CountRobotCore(threshold, cols, rows, col+1, row, visited)
			+ CountRobotCore(threshold, cols, rows, col, row-1, visited)
			+ CountRobotCore(threshold, cols, rows, col, row+1, visited)return res;
}

int CountRobot(int threshold, int rows, int cols)
{
	if(rows<0 || cols<0 || threshold<0)
		return 0;
	
	bool* visited = new bool[cols*rows];
	memset(visited,0,rows*cols);

	int result = CountRobotCore(threshold, cols, rows, 0, 0, visited);

	delete[] visited;
	return res;

}
面试题14:剪绳子
// 思路:采用动态规划(DP)或者贪心算法

// 1. 动态规划
int maxDPCut(int length)
{
	// 由于最少必须减一次
	if(length<2) return 0;
	if(length == 2) return 1;
	if(length == 3) return 3;

	// 当绳子长度大于等于4时,不减绳子的收益大于剪绳子,因此需要重新给小于4的赋值
	vector<int> product{0,1,2,3};

	int max = 0;
	for(int i=4; i<=length; i++)
	{
		max = 0;
		for(int j=1; j<=i/2; j++) // 分为两份,防止重复计算
		{
			int products = product[j]*product[i-j];
			if(max<products)
				max = products;
		}
		product.push_back(max);
	}
	return product[length];
}
面试题15:二进制中1的个数

总结:

  1. 位运算包含5种运算:与、或、异或、左移、右移;
  2. 左移:左边丢弃,右边补0;右移:右边丢弃,左边根据是否具有符号确定补0或1;
  3. 尽量使用移位操作替代2的整数此房的乘除法,使用位与运算判断一个数的奇偶性(n&1);
  4. 把一个整数减一后再和原来整数做位与运算,得到的结果相当于把原来整数的二进制表示中最右边的1变成0;
//思路:可以采用自身减一与自身与操作来消除1,此操作便可以用来记录1的个数

int NumberOf1(int n)
{
	int count = 0;
	while(n)
	{
		count++;
		n = n & (n-1);
	}
	return count;
}

// 输入两个整数m,n,计算需要改变m的二进制表示中的多少位才能得到n.
int getNumberof1(int m, int n)
{
	int count = 0;
	int data = m^n;  // 使用异或将两者转换为不同位为1, 接下来统计1的个数
	while(data)
	{
		count++;
		data = data & (data-1);
	}
	return count;
}

3. 高质量的代码

面试题16:数值的整数次方
// 思路:需要考虑底数为0,指数为负数的情况

double Power(double base, int exponent)
{
	if(base == 0.0 && exponent<0)  // 底数为0且指数为负数,直接返回0.0;
		return 0.0;
	
	if(exponent== 0)
		return 1.0;
		
	int absExp = exponent <0 ? -exponent : exponent;
	double result = 1.0;
	for(int i=0; i<absExp; i++)
	{
		result *= base;
	}
	if(exponent <0)
		result = 1.0/result ;

	return exponent <0 ? 1.0/result : result;
}


// 递归算法
double pow(double base, int exps)
{
	if(exps == 0) return 1.0;

	double res = pow(base, exps>>1);
	if(exps&1 == 1)
		return res * res * base;
	else 
		return res * res;
}

double Power(double base, int exponent)
{
	if(base == 0.0)	return 0.0;
	
	int AbsExp = exponent < 0 ? -exponent : exponent ;

	return exponent < 0 ? 1.0/pow(base, AbsExp) : pow(base, AbsExp);	
}
面试题17:删除链表的结点
// 思路:判断头结点是否是要删除的结点,如果是,则返回下一个节点;否则判断下一个节点是要要删除,以此类推
class ListNode
{
public:
	int val;
	ListNode* next;

	ListNode(int v):val(v),next(nullptr) { }
};

ListNode* deleteNode(ListNode* head, int val)
{
	if(head->val == val) return head->next;

	ListNode* node = head;
	whiel(node->next != nullptr)
	{
		if(node->next->val == val)
		{
			node->next = node->next->next;
			return head;
		}
		node = node->next;
	}
	return head;
}
// 思路: 删除链表中重复的数字.
// 首先创建链表,该链表的下一个节点指向头指针,并创建pre与cur用于遍历和删除节点
ListNode* deleteDupli(ListNode* head)
{
	if(head==nullptr) return head;
	ListNode* dyNode = new ListNode(-1);
	dyNode->next = head;
	ListNode* pre = dyNode;
	ListNode* cur = head;

	while(cur)
	{
		if(cur->next && cur->next->val == cur->val) // 如果遇到重复节点
		{
			while(cur->next && cur->next->val == cur->val)  // 遍历跳过
				cur= cur->next;
			pre->next = cur->next; // 删除重复的结点
		}
		else  // 如果没遇到重复结点
		{
			pre = cur;  // 移动前驱结点
		}
		cur = cur->next; // 不管有没有重复节点,都移动cur
	}
	return dyNode->next; //返回链表的下一个节点(不包括头结点)
}
面试题18:调整数组顺序使奇数位于偶数前面
// 思路:使用双指针遍历,左边指针如果是奇数,则叠加;右边指针是偶数,则叠加
vector<int> recorder(vector<int>& vec)
{
	int left = 0;
	int right = vec.size()-1;
	while(left<right)
	{
		if((vec[left]&1) == 1)
		{
			left++;
			continue;
		}

		if((vec[right]&1) == 0)
		{
			right--;
			continue;
		}
		swap(vec[left++], vec[right--]);
	}
	return vec;
}

面试题19:链表中倒数第k个节点
// 思路:使用两个指针,保证快指针移动k个节点,慢指针在快指针移动k个节点后开始移动,直到快指针为空指针
ListNode* FindKnode(ListNode* head, int k)
{
	if(head == nullptr || k<=0)
	{
		return nullptr;
	}
	ListNode* pre = head;
	ListNode* cur = head;
	for(int i=0; i<k; i++)
	{
		if(cur!=nullptr)
		{
			cur = cur->next;
		}
		else
		{
			return nullptr;  // 说明k大于链表长度
		}
	}
	while(cur!=nullptr)
	{
		cur = cur->next;
		pre = pre->next;
	}
	return pre;	
}
// 快慢指针遍历
ListNode* midNode(ListNode* head)
{
	if(head == nullptr) return nullptr;
	ListNode* fast = head;
	ListNode* slow= head;
	while(fast && fast->next)
	{
		fast = fast->next->next;
		slow= slow->next;
	} 
	return sold;
}
面试题23:链表中环的入口节点
// 思路:快指针路径是慢指针路径的2倍,使用快慢指针找到相遇节点,然后从头开始遍历链表,直到和slow相遇,返回慢指针节点
ListNode* EntryLoop(ListNode* head)
{
	if(head == nullptr) return nullptr;
	
	ListNode* fast=head;
	ListNode* slow=head;
	while(fast && fast->next)
	{
		fast = fast->next->next;
		slow = slow->next;
		if(fast==slow)
		{
			ListNode* node = head;
			while(node != slow)  // 两者路径长度相同  (a+b+c+b) = 2(a+b)  => a = c
			{
				node = node->next;
				slow = slow->next;
			}
			return slow;  // 找到环入口即返回
		}
	}
	return nullptr;   // 表示没有环
}
面试题24:反转链表
// 思路:使用三个指针进行保存,分别为当前指针、前一个指针、后一个指针
ListNode* reverseList(ListNode* head)
{
	ListNode* cur = head;
	ListNode* pre = nullptr;
	ListNode* next = nullptr;

	while(cur != nullptr)
	{
		next = cur->next;
		cur->next = pre;
		pre = cur;
		cur = next;
	}
	return pre;
}
面试题25:合并两个排序的链表
// 思路:采用非递归的方式,按照大小顺序进行递增
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
{
	ListNode* node = new ListNode(-1);
	ListNode* retNode = node;
	
	while(l1 && l2)
	{
		if(l1->val > l2->val)
		{
			node->next = l2;
			l2 = l2->next;
		}
		else
		{
			node->next = l1;
			l1 = l1->next;
		}
		node = node->next;
	}
	if(l2 == nullptr) node->next = l1;
	if(l1 == nullptr) node->next = l2;

	return retNode->next;
}
面试题25.1:合并k个已排序的链表
// 思路:属于上一个的变种。只需要遍历即可
ListNode* mergeKLists(vector<ListNode*> &lists)
{
	int len = lists.size();
	if(len == 0) return nullptr;
	ListNode* head = lists[0];
	for(int i=1; i<len; i++)
	{
		
	}
}

ListNode* twoLists(ListNode* l1, ListNode* l2)
{
	ListNode* node = new ListNode(-1);
	ListNode* n = node;
	while(l1 && l2)
	{	
       if(l1->val > l2->val)
       {
           node->next = l2;
           l2 = l2->next;
       }
       else{
           node->next = l1;
           l1 = l1->next;
       }
       node = node->next;
     }
     node->next = (l1==nullptr)? l2 : l1;
     return n->next;
}
面试题26:树的子结构
// 思路:使用递归的方式,两种递归,1是递归遍历寻找与子树根结点相同;2是找到与子树根结点相同后,开始遍历比较两个子树,递归遍历
class TreeNode
{
	int val;
	TreeNode* left;
	TreeNode* right;
	TreeNode(int v) : val(v), left(nullptr), right(nullptr) { }
};
// 主函数,用于递归查找与根结点是否相同
bool SubTree(TreeNode* A, TreeNode* B)
{
	if(A==nullptr || B==nullptr) return false;

	bool SubFlag = false;
	if(A->val == B->val) SubFlag = AhaveB(A, B);
	if(!SubFlag) SubFlag = SubTree(A->left, B);
	if(!SubFlag) SubFlag = SubTree(A->right, B);

	return SubFlag;
}

// 子函数:用于递归比较与子树是否相同
bool AhaveB(TreeNode* A, TreeNode* B)
{
	if(B==nullptr) return true;
	if(A==nullptr) return false;

	if(A->val != B->val) return false;

	return AhaveB( A->left,  B->left) && AhaveB( A->right,  B->right);
}
面试题27:二叉树的镜像
// 思路:递归遍历,先交换左右子树的结点,然后进行左右子树递归
TreeNode* mirroeTree(TreeNode* root)
{
	if(root == nullptr) return nullptr;   // 递归结束条件

	TreeNode* temp = root->left;   // 交换左右子树
	root->left = root->right;
	root->right = temp;

	mirrorTree(root->left);  // 递归左子树
	mirrorTree(root->right); // 递归左子树

	return root;
}
面试题28:对称的二叉树
// 思路:判断二叉树是否为对称的,即左子树的左 == 右子树的右, 左子树的右 == 右子树的左
// 主函数
bool isSymmetric(TreeNode* root)
{
	if(root == nullptr) return true;
	return Mirror(root->left, root->right);
}

// 子函数
bool Mirror(TreeNode* left, TreeNode* right)
{
	// 先判断结点为空的情况
	if(left==nullptr && right==nullptr) return true;
	if(left==nullptr || right==nullptr) return false;

	// 判断结点不为空时,结点值不同
	if(left->val != right->val) return false;

	bool outSide = Mirror(left->left, right->right);
	bool inSide = Mirror(left->right, right->left);
	return outSide && inSide;
}
面试题29:顺时针打印矩阵
// 思路:首先判断矩阵是否为空,然后在开始打印,按照:从左到右,从上到下,从右到左,从下到上的顺序
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        
        if(matrix.size()==0 || matrix[0].size()==0) return {};

        int rows = matrix.size();
        int cols = matrix[0].size();

        vector<int> result;
        int row = 0, col = -1;
        while(rows>0 && cols>0)
        {
            // 从左到右
            for(int i=0; i<cols; i++)
            {
            	col++;
                result.push_back(matrix[row][col]);
            }
            rows--;
            if(rows==0 || cols==0) break;  // 一旦存在行或列为空,则需要跳出来

            // 从上到下
            for(int i=0; i<rows; i++)
            {
            	row++;
                result.push_back(matrix[row][col]);
            }
            cols--;
            if(rows==0 || cols==0) break;

            // 从右到左
            for(int i=0; i<cols; i++)
            {
            	col--;
                result.push_back(matrix[row][col]);
            }
            rows--;
            if(rows==0 || cols==0) break;

            // 从下到上
            for(int i=0; i<rows; i++)
            {
            	row--;
                result.push_back(matrix[row][col]);
            }
            cols--;
            if(rows==0 || cols==0) break;
        }

        return result;
    }
面试题30:包含min函数的栈
// 思路:添加一个辅助栈,用来保存每添加一个数据时的最小值
class MinStack {
public:
    /** initialize your data structure here. */
    stack<int> m_data, m_min;

    MinStack() {   }
    
    void push(int x) {
        m_data.push(x);
        if(m_min.empty())
        {
            m_min.push(x);
        } 
        else
        {
            int temp = m_min.top();
            if(temp > x) 
                m_min.push(x);
            else 
                m_min.push(temp);
        }
    }
    
    void pop() {
        m_data.pop();
        m_min.pop();
    }
    
    int top() {
        return m_data.top();
    }
    
    int min() {
        return m_min.top();
    }
};
面试题31:栈的压入、弹出序列
// 思路:添加一个辅助占, 每次想辅助栈中添加元素,都需要用栈顶数据跟弹出序列的数据进行比较,如果相等,栈就需要出栈顶;同时弹出序列向后移动。压入和弹出序列相等的条件是,辅助栈为空。
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {

        bool result = false;
        if(pushed.size() != popped.size()) return result;

        stack<int> m_data;
        int idx = 0;
        for(auto& vec : pushed)
        {
            m_data.push(vec);
            while(!m_data.empty() && m_data.top() == popped[idx])
            {
                m_data.pop();
                idx++;
            }
        }

        if(m_data.empty())  result = true;
        
        return result; 
    }
面试题32:从上到下打印二叉树
// 思路, 使用队列想入先出的方式,不断加入节点
vector<int> levelOrder(TreeNode* root)
{
	if(root == nullptr) return {};

	queue<TreeNode*> que;
	que.push(root);
	vector<int> result;

	while(!que.empty())
	{
		TreeNode* node = que.front();
		que.pop();

		result.push_back(node->val);

		if(node->left != nullptr) que.push(node->left);
		if(node->right != nullptr) que.push(node->right);
	}
	return result;
}



// 思路:打印二叉树,每层打印到一行
vector<vector<int>> levelOrder(TreeNode* root)
{
	if(root == nullptr) return {};

	queue<TreeNode*> que;
	que.push(root);
	vector<vector<int>> result;

	while(!que.empty())
	{
		vector<int> temp;
		int sz = que.size();

		while(sz--)
		{
			TreeNode* node = que.front();
			temp.push_back(node->val);
			que.pop();

			if(node->left != nullptr) que.push(node->left);
			if(node->right != nullptr) que.push(node->right);
		}
		result.push_back(temp);
	}
	return result;
}


// 思路:之字型打印二叉树; 需要注意偶数层需要反转,奇数层不变
vector<vector<int>> levelOrder(TreeNode* root) {
    if(root == nullptr) return {};

    queue<TreeNode*> que;
    que.push(root);
    int level = 0;  // 用于记录层数
    vector<vector<int>> result;

    while(!que.empty())
    {
        int sz = que.size();
        vector<int> temp;

        level++;
        while(sz--)
        {
            TreeNode* node = que.front();
            temp.push_back(node->val);
            que.pop();

            if(node->left != nullptr) que.push(node->left);
            if(node->right != nullptr) que.push(node->right);
        }                
        if((level & 1) == 0) reserve(temp); // 偶数需要反转
        result.push_back(temp);
    }
    return result;
}

void reserve(vector<int>& vec)
{
     int sz = vec.size();
     for(int i=0; i<sz/2; i++)
     {
         int temp = vec[i];
         vec[i] = vec[sz-i-1];
         vec[sz-i-1] = temp;
     }
}
面试题33:二叉搜索树的后序遍历序列
// 思路:判断二叉搜索树(二叉排序树)是否是后序遍历(左右根)

// 主函数
bool versfyPostorder(vector<int>& postorder)
{
	if(postorder.size() == 0) return true;
	return dfsOrder(postorder, 0, postorder.size()-1);
}

bool dfsOrder(vector<int> vec, int left, int right)
{
	if(left >= right) return true;

	int root = vec[right];
	int i = left;
	for(; i<right; i++)  // 找到比根结点大的第一个结点位置
	{
		if(vec[i] > root)
			break;
	}

	for(int j=i; j<right; j++)  // 如果右子树存在比根结点小的数,则说明该序列不是后续遍历序列
	{
		if(vec[j] < root)
			return false;
	}

	return defOrder(vec, left, i-1) && defOrder(vec, i, right-1); // 递归遍历左右子树
}
面试题34:二叉树中和为某一值的路径
// 思路:使用前序递归的形式遍历二叉树,保存路径,然后比较路径之和。最后将所有路径上的数字保存

vector<vector<int>> FindPath(TreeNode* root, int sum)
{
	if(root == nullptr) return {};
	vector<int> path;
	vector<vector<int>> result;

	findPath(root, sum, path, result);
	return result;
}

void findPath(TreeNode* root, int sum, vector<int>& path, vector<vector<int>>& res)
{
	// 1. 对根结点保存
	path.push_back(root->val);

	// 2. 判断当前是否满足和要求
	if(root->val == sum && root->left==nullptr && root->right==nullptr)
	{
		res.push_back(path);
	}
	// 3. 不满足和条件,就开始遍历左子树和右子树(需要注意,此时sum改变,需要减掉当前节点的值)
	if(root->left) findPath(root->left, sum-root->val, path, res);
	if(root->right) findPath(root->left, sum-root->val, path, res);
	path.pop_back();
}
面试题35:复杂链表的复制
// 思路:可以使用哈希表进行拷贝复制
class Node
{
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {val = _val; next = NULL; random = NULL;}
};

Node* copyRandomList(Node* head)
{
	if(head == nullptr) return nullptr;
	// 1. 创建哈希表,并遍历链表
	unordered_map<Node*, Node*> u_map;
	Node* cur = head;
	while(curr != nullptr)
	{
		u_map[cur] = new Node(cur->val);
		cur = cur->next;
	}
	// 2. map映射复制
	cur = head;
	while(cur != nullptr)
	{
		u_map[cur]->next = u_map[cur->next];
		u_map[cur]->random = u_map[cur->random];
		cur = cur->next;
	}
	return u_map[head];
}
面试题36:二叉搜索树与双向链表
// 思路:由于是二叉搜索树,因此需要采用中序遍历。
class Solution{
public:
	Node* treeToDoublyList(Node* root)
	{
		if(root == nullptr) return nullptr;
		inOrder(root);
		head->left = pre;
		pre->right = head;
		
		return head;
	}

private:
	Node* pre;
	Node* head;
	void inOrder(Node* cur)
	{
		if(cur == nullptr) return ;

		// 中序遍历
		inOrder(cur->left);
		
		if(pre != nullptr) pre->right = cur;
		else head = cur;
		cur->left = pre;
		pre = cur;
		
		inOrder(cur->right); 
	}
};
面试题37:在排序数组中查找数字
// 思路:可用哈希表,或者 二分法查找。 其中哈希表较为占用内存

int search(vector<int>& nums, int target)
{
	return BinaryLeft(nums, target+1)-BinaryLeft(nums, target);
}

int BinaryLeft(vector<int>& nums, int target)
{
	int left = 0;
	int right = nums.size()-1;
	int mid = 0;
	while(left<=right)
	{
		mid = (left+right)>>1;
		if(nums[mid] < target)
		{
			left = mid+1;
		}
		else
		{
			right = mid-1;
		}
	}
	return left;
}
面试题38: 0~n-1中缺失的数字
// 思路:使用二分法查找,如果相等则在右边,否则在左边

void missingNumber(vector<int>& nums)
{
	return BinaryNum(nums);
}

int BinaryNum(vector<int> nums)
{
	int left = 0;
	int right = nums.size()-1;
	int mid = 0;
	while(left<=right)
	{
		mid = (left+right)>>1;
		if(nums[mid] == mid)
			left = mid + 1;
		else
			right = mid - 1;
	}
	return left;
}
面试题39:二叉搜索树的第k大节点
// 思路: 使用中序遍历可以得到二叉搜索树按照从小到大的排序,而我们需要从大到小的排序,因此需要按照从右到左

int kthLargest(TreeNode* root, int k)
{
	if(root == nullptr || k==0) return 0;
	vector<int> result;
	mid_dfs(root, result);

	return result[k-1];
	
}

void mid_dfs(TreeNode* root, vector<int>& vec)
{
	if(root == nullptr) return ;
	mid_dfs(root->right, vec);
	vec.push_back(root->val);
	mid_dfs(root->left, vec);
}
面试题40:二叉树的深度
// 思路: 采用层序遍历,借助queue

int maxDepth(TreeNode* root)
{
	if(root == nullptr) return 0;

	queue<TreeNode*> que;
	que.push(root);
	int depth=0;
	while(!que.empty())
	{
		++depth;
		int size = que.size();
		while(size--)
		{
			TreeNode* node = que.front();
			que.pop();
			if(node->left) que.push(node->left);
			if(node->right) que.push(node->right);
		}
	}
	return depth;
}
面试题41:判断平衡二叉树
// 思路:如果时平衡二叉树,则说明任意节点的左、右子树的高度差不超过1.可以使用后序遍历。

bool res = true;
bool idBalance(TreeNode* root)
{
	dfs(root);
	return res;
}

int dfs(TreeNode* root)
{
	if(root == nullptr) return 0;
	int left = dfs(root->left);
	int right = dfs(root->right);
	if(abs(left-right)>1) res = false;
	return max(left, right)+1;
}
面试题42:数组中数字出现的次数
// 思路:可以使用异或实现检测数组中出现不重复的数字,如果一个数组中只有一个不重复的数字,仅需要遍历一遍即可。

vector<int> singleNumbers(vector<int>& nums)
{
	int first=0, second=0, mid=1;
	for(auto &n : nums) // 遍历一遍数组可以找到两个不重复数字的组合
	{
		first ^= n;
	}
	while( (first&mid) == 0) // 找到两个数字第一个不同的位,利用该位将数组分为两份,然后分别查找
		mid <<=1;
	first = 0;
	for(auto& n : nums)
	{
		if(n&mid) first ^= n;
		else second ^= n; 
	}
	return {first, second};
}
面试题43:数组中唯一出现一次的数字
// 思路:可以使用哈希表

int singleNumber(vector<int>& nums)
{
	int res = 0;
	unordered_map<int, int> map_n;
	for(auto &n:nums) map_n[n]++;
	for(auto &n:nums)
	{
		if(map_n[n] == 1)
		{
			res = n;
			break;
		}
	}
	return res;
}
面试题44:和为s的两个数
// 思路:使用双指针,前后各一个指针。循环向前逼近
vector<int> twoSum(vector<int>& nums, int target)
{
	int begin = 0;
	int end = nums.size()-1;

	while(end > begin)
	{
		int res = nums[begin]+nums[end];
		if(res > target) end--;
		else if(res < target) begin++;
		else return vector<int>{nums[begins], nums[end]};
	}
	return vector<int>();
}
面试题45:和为s的连续正整数序列
// 思路:可以使用滑动窗口算法,左闭右开。主要判断条件时 左边界小于目标值的一半

vector<vector<int>> findContinuousSequence(int target)
{
	int left = 1; // 左边界
	int right = 1; // 右边界
	int sum = 0; // 中间值
	vector<vector<int>> result;

	while(left <= target/2)
	{
		if(sum < target)
		{
			sum += right;
			right++;
		}
		else if(sum > target)
		{
			sum -= left;
			left++;
		}
		else{
			vector<int> res;
			for(int i=left; i<right; i++)
			{
				res.push_back(i);
			}
			result.push_back(res);
			sum -= left;
			left++;
		}
	}
	return result;
}
面试题46:翻转单词顺序
// 思路:采用反转单词顺序,同时需要按照:完全反转句子顺序,去除多余空格(多于2个空格),完全翻转后在对每一个单词反转。

string reverseWords(string s)
{
	int begin = 0; // 起始反转位置
	int end = s.size()-1; // 终止反转点
	reserve(s, begin, end); // 反转整个句子

	begin = 0;
	end = 0;
	while(s[end] == '\0') // 如果没有到字符串最后一位
	{
		if(s[begin] == ' ')
		{
			s.erase(begin,1);  // 移除多余的空格
		}
		else if(s[end] == ' ')
		{
			reserve(s, begin, end-1);
			end++;
			begin = end;
		}
		else
		{
			end++;
		}
	}
	if(end>1 && s[end-1] == ' ') s.erase(end-1,1); // 移除最后一个空格
	reserve(s, begin, end-1); // 少翻转一次,需要补上(最后一个单词没有反转)
	return s;
	
}

// 反转单词顺序
void reserve(string &s, int begin, int end)
{
	int mid = (begin+end)>>1;
	while(begin <= mid)
	{
		char c = s[begin];
		s[begin] = s[end];
		s[end] = c;
		begin++;
		end--;
	}
}
面试题47:左旋转字符串
// 思路:1.可以使用反转字符串的思路,即先反转前半部分字符串,然后反转后半部分字符串,最后反转整个字符串。2. 或者直接使用字符串拼接,前后拼接,取其中从n到len的字符串

// 1. 反转字符串思路
string reserveLeftWords(string s, int n)
{	
	if(s.size() <=1 )return s;
	int end = s.size()-1;
	reserve(s, 0, n-1);  // 反转前半部分
	reserve(s, n, end);  // 反转后半部分
	reserve(s, 0, end);  // 反转全部

	return s;
}
void reserve(string &s, int begin, int end)
{	
	int mid = (begin+end)>>1;
	while(begin <= mid)
	{	
		char c = s[begin];
		s[begin] = s[end];
		s[end] = c;
		begin++;
		end--;
	}
}

// 2. 拼接方法
string reserveLeftWords(string s, int n)
{
	int len = s.size();
	s += s;  // 拼接字符串
	return s.substr(n, len); // 取出(n-len)字符串
}
面试题48:滑动窗口的最大值
// 思路:可以使用双端队列实现查找,具体可以看下面代码
vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
	vector<int> result;
	deque<int> que;

	for(int i = 0; i<nums.size(); i++)
	{
		// 查找滑窗内的最大值,放到队头
		while( !que.empty() && nums[i] > num[que.back()])
			que.pop_back();
		// 判断队头的索引是否超出窗口左边界
		if( !que.empty() && que.front() < i-k+1)
			que.pop_front();
		que.push_back(i);

		if( i >= k-1) result.push_back(nums[que.front()]);
	}
	return result;
}
面试题49:队列中的最大值
// 思路:使用单调不增序列,同时使用队列和双向队列

class MyQueue{

public:
	queue<int> que;
	deque<int> deq;

	MyQueue()
	{
	
	}

	int MaxValue()
	{
		if(deq.empty()) return -1;
		return deq.front();
	}

	void push_back(int value)
	{
		que.push(value);
		while(!deq.empty() && deq.back() < value)
			deq.pop_back();
		deq.push_back(value);
	}

	int pop_front()
	{
		if(que.empty()) return -1;
		int value = que.front();
		if(value == deq.front())
			deq.pop_front();
		que.pop();
		return value;
	}
};
面试题60:n个骰子的点数
// 思路:主要利用动态规划进行求解,其状态转移方程需要构建,还有初始化边界条件

vector<double> dicesProbability(int n)
{
	// 首先定义:二维数组res, 表示第n个骰子和为j出现的次数
	vector<vector<int>> res(n+1, vector<int>(6*n+1, 0));
	// 其次定义初始化条件
	for(int i=1; i<=6; i++)
		res[1][i] = 1;
	// 之后开始循环,使用上一时刻的状态定义下一时刻
	for(int i=2; i<=n; i++)
		for(int j=2; j<=6*n; j++)
			for(int k=1; k<=6; k++)
			{
				if(j<=k) break;
				res[i][j] += res[i-1][j-k];
			}
	int allSum = pow(6,n); // 表示一共计算的次数,用于计算概率
	vector<double> result; // 保存计算结果
	// 下面便是将第n个骰子和为j的次数除以总计算次数得到概率
	for(int i=n; i<=6*n; i++) // 由于和的范围是[n,6*n]
	{
		result.push_back(res[n][i]*1.0/allSum);
	}
	return result;
}
面试题61:扑克牌中的顺子
// 思路:使用先排序,在统计0的个数,之后则判断相邻间数字的间隔与0的大小

bool isStraight(vector<int>& nums)
{
	sort(nums.begin(), nums.end());
	int zero_size = 0;
	int space_sum = 0;
	for(int i=0; i<4; i++)
	{
		if(nums[i] == 0)
		{
			zero_size++;
			continue;
		}
		int temp = nums[i+1]-num[i];  // 计算数字间隔
		if(temp > 1)
			space_sum += (temp-1); // 统计相邻数字间的间隔,正常间隔为1
		if(temp == 0)
			return false;  // 存在重复数字,直接返回
	}

	return (zero_size >= space_sum);
}
面试题62:股票的最大利润
// 思路:只需要一次遍历即可,记录最大利润

int maxProfit(vector<int>& prices)
{
	if(prices.size() < 2) return 0; // 少于两个值,输出为0

	int min_value = prices[0];  // 记录前面的数据的最低值(买入值)
	int max_price = prices[1] - min_value;  // 记录最大利润

	for(int i=1; i<prices.size(); i++)  // 一次遍历
	{
		if(prices[i] < min_value) 
			min_value = prices[i];
		int dif = prices[i]-min_value;
		if(dif > max_price)
			max_price = dif;
	}
	return max_price;
}
面试题63:求解1+2+…+n
// 思路:由于不能使用乘除,for、 if等运算符和关键字. 可以使用递归或者计算内存的方式
int sumNums(int n)
{
	n && (n += sumNums(n-1));
	return n;
}

// 利用计算内存 sizef
int sumNums(int n)
{
	bool a[n][n+1];
	return (sizeof(a)>>1);
}
面试题64:不用加减乘除做加法
// 思路:使用 位运算进行计算, 分别计算 进位和没进位的加法
// 进位 (a&b)<<1;
// 没进位的加法 a^b;

int add(int a, int b)
{
	while(b != 0)  // 进位不为0
	{
		int c = (unsigned int)(a&b)<<1;
		a = a^b;
		b = c;
	}
	return a;
}
面试题65:树中两个节点的最低公共祖先
// 思路:由于是二叉搜索树, 因此可以同时比较树的值
// 如果当前节点比两个节点值都大,说明公共节点在左子树;如果比两个节点都小,说明公共节点都在右字树。其他情况说明当前节点就是公共节点或者公共节点是两者中的一个
TreeNode* LowerCommon(TreeNode* root, TreeNode* p, TreeNode* q)
{
	TreeNode* node = root;
	while(true)
	{
		if(node->val > p->val && node->val > q->val)
			node = node->left;
		else if(node->val < p->val && node->val < q->val)
			node = node->right;
		else
			break;
	}
	return node;
}
面试题66:二叉树的公共祖先
// 思路:由于是普通二叉树, 需要遍历查找。采用后序遍历
TreeNode* lowestCommon(TreeNode* root, TreeNode* p, TreeNode* q)
{
	if(!root || !p || !q || root==p || root=q) // 表示当前当前节点就是两个节点中一个
		return root;
	
	TreeNode* left = lowestCommonAncestor(root->left, p, q);
	TreeNode* right = lowestCommonAncestor(root->right, p, q); // 后序遍历

    if(!left && !right) return nullptr;  // 表示节点两边都没有
    else if(left && right) return root;  // 表示在当前节点两边
    else if(!left && right) return right;  // 表示在右节点这边
    else return left;   // 表示在左节点这边
}
面试题67:删掉一个元素以后全为 1 的最长子数组
// 思路:可以使用滑动窗口进行统计,主要统计滑动窗口内0的个数。如果0个数大于1则需要移动左边界,将多余的0移除出去;如果0个数小于等于1则需要移动右边界

int longestSubarray(vector<int>& nums)
{
	// 首先定义左右边界
	int left=0, right=0;
	int size=nums.size(); // 统计数组的大小
	int count=0;  // 表示窗口内0的个数
	int res=0; // 表示最后的滑窗内最大连续1值

	while(right<size)  // 移动到右边界
	{
		count += nums[right]==0;  // 统计滑窗内0的个数
		while(count >1)
		{
			count -= nums[left]==0; // 将移除掉的0剪掉
			left++;
		}
		res = max(res, right-left+1);  // 保存最大窗口数
		right++;  // 说明滑窗内0个数小于等于1
	}
	return res-1;  // 由于需要减掉0,因此需要为窗口最大值减1
}


// 包含输入输入输出的笔试
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

// 滑动窗口算法
int maxSlideWindow(vector<int> nums)
{
	int left = 0, right = 0;
	int count = 0;
	int size = nums.size();
	int res = 0;

	while (right < size)
	{
		count += nums[right] == 0;
		while (count > 1)
		{
			count -= nums[left] == 0;
			left++;
		}
		res = max(res, right - left + 1);
		right++;
	}
	return res - 1;
}

// 主函数
int main()
{

	int count = 0;
	cin >> count;

	vector<vector<int>> nums;
	while (count--)
	{
		int signlCount = 0;
		cin >> signlCount;
		int temp = 0;
		vector<int> nums1;
		while (signlCount--)
		{
			cin >> temp;
			nums1.push_back(temp);
		}
		nums.push_back(nums1);
	}
	int length = nums.size();

	for (auto & num : nums)
	{
		int res = maxSlideWindow(num);
		cout << res << " ";
	}
	cout << endl;
	
	//system("pause");
	return 0;
}
面试题68:分割回文串(阿里实习笔试)
// 思路,使用动态规划
// d[n][k]表示前n个字符被分割为k个回文子串至少需要修改的字符数

// 1. 判断回文子串所需要修改的字符数
int cost(string& s, int l, int r)
{
	int res = 0;
	for(int i=l, j=r; i<j; i++, j--)
	{
		if(s[i] != s[j])
			res++;
	}
	return res;	
}

// 2. 动态规划返回d[n][k] 
int reslotion(string& s, int k)
{
	int n=s.size(); // 字符个数
	// dp[i][j]表示前i个字符分割为j个子串需要的最少字符数,其中i>=j
	vector<vector<int>> dp(n+1, vector<int>(k+1, 1e6));

	dp[0][0] = 0;  // 状态初值
	for(int i=1; i<=n; i++) // 前i个字符串
	{
		for(int j=1; j<=min(i,k); j++)  // 表示分割为j个回文子串, 且j<=i以保证足够分割
		{
			if(j==1)  // 表示前i个不分割,直接统计
			{
				dp[i][j] = cost(s, 0, i-1);
			}
			else
			{
				for(int t=j-1; t<i; t++)   // 将j个子串再分为 j-1个子串和1个回文串之和,以此类推
				{
					dp[i][j] = min(dp[i][j], dp[t][j-1]+cost(s, t, i-1));  // 状态转移方程,使用最小的
				}
			}
		}
	}
	return dp[n][k];
}

int main()
{
	int size = 0;
	cin>>size;
	
	vector<string> str;
	vector<int> count;
	int temp;
	while(size--)
	{
		string temp_str;
		cin>>temp_str;
		str.push_back(temp_str);
		cin>>temp;
		count.push_back(temp);
	}

	for(int i=0; i<str.size(); i++)
	{
		cout<<reslotion(str[i], count[i])<<endl;
	}
	return 0;
}
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值