【Code Pratice】—— 《图解算法数据结构 ~ 第一章》
简述
本文主要记录了学习《图解算法数据结构》一书中“数据结构”章节所做练习题的笔记,记录其中的思路以及碰到的问题等。因为学习的这本书是在leetcode上的,但是感觉leetcode上只要求做函数本身的实现,而无法做到对总体的把控,总感觉缺少了一部分,所以所有的题目中,为了追求编程的完整性(头文件、数据结构、函数定义、函数实现),练习的时候不局限于题目的要求,增加一点适应性,总的代码文件会放到文章末尾以供后续回看。
文章目录
- 1 | 剑指Offer05:替换空格
- 2 | 剑指Offer06:从尾到头打印链表
- 3 | 剑指Offer09:用两个栈实现队列
- 4 | 剑指Offer20:表示数值的字符串
- 5 | 剑指Offer24:反转链表
- 6 | 剑指Offer30:包含min函数的栈
- 7 | 剑指Offer35:复杂链表的复制
- 8 | 剑指Offer58:左旋转字符串
- 9 | 剑指Offer59:滑动窗口的最大值
- 10 | 剑指Offer59:队列的最大值
- 11 | 剑指Offer67:把字符串转换成整数
1 | 剑指Offer05:替换空格
题目
将字符串A中的所有空格替换为%20
例子
A = We are family!
Result = We%20are%20family!
题目完整性
思路
只需要找到每一个空格的位置,并将空格替换成对应字符串即可,问题在于
怎么找到每一个空格的位置?
查找字符串中某字符位置都知道使用string.find()函数,但是这一系列的find()函数对于同一个字符串每次只能找第一个或最后一个出现该字符的位置。
既然每次只能找首位,那么就让第一个空格变成第一个空格 --> 每找到一个空格,就把该空格前的字符串取出来,再删除原字符串中的取出来的部分和这个空格,那么下一次查找时的空格就是第二个空格,依次循环查找
怎么替换空格?
上面每一次查找空格时,都保留了空格前的字符串,只需要在保存的字符串后都接上替换的字符即可
代码
void ReplaceSpaces(string& i_cStr, const string& i_cCha) {if (("" == i_cStr) || ("" == i_cCha)){return;}string tmp = "";size_t pos = i_cStr.find(' ');while (pos != i_cStr.npos){tmp += i_cStr.substr(0, pos);tmp += i_cCha;i_cStr = i_cStr.substr(pos + 1, i_cStr.size());pos = i_cStr.find(' ');}tmp += i_cStr;i_cStr = tmp; }问题
在写主体输入部分的时候,输入一行带多个空格的字符串时碰到一点之前忽略的问题,getline() 和 cin.getline() 两个函数的区别
- 如果定义了一个string类型的变量,那么只能使用getline(),不能使用cin.getline(),即使在参数调用的时候强转成char*类型也没用,具体待看源码后记录
- getline()默认等到输入结束符后从缓存区中读取完整的字符串,不需要指定输入的长度,而cin.getline()需要指定输入的长度,所以从实用性来说,getline()要比cin.getline()实用,而且cin.getline()可以进行缓存区溢出漏洞的利用
2 | 剑指Offer06:从尾到头打印链表
题目
给定一个链表的头节点,从尾到头的输出这个链表各节点的值
例子
head->1 2 3
输出 3 2 1
思路
从尾到头无非就是反转的意思
代码
vector<int> FromTail2HeadPrintLinkList(ListNode* head) {if (NULL == head){return;}ListNode* tmp = head;vector<int> res;while (tmp){res.push_back(tmp->val);tmp = tmp->next;}for (int i = 0, j = res.size() - 1; i < j; i++, j--){int temp = res[i];res[i] = res[j];res[j] = temp;}return res; }3 | 剑指Offer09:用两个栈实现队列
题目
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
示例
输入1:
[“CQueue”,“appendTail”,“deleteHead”,“deleteHead”,“deleteHead”]
[[],[3],[],[],[]]
输出1:
[null,null,3,-1,-1]
输入:
[“CQueue”,“deleteHead”,“appendTail”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[],[5],[2],[],[]]
输出:
[null,-1,null,null,5,2]
思路
栈的特性“先进后出”,队列的特性“先进先出”
要用栈实现队列就是构造出队列的特性,而两个栈可以模拟这个特性,思路如下
- 栈1:1 2 3 4 5; 栈2:
- 队列尾部加入元素:
- 栈1入栈新元素 --> 栈1:1 2 3 4 5 6; 栈2:
- 此时执行队列输出时,将栈1的元素出栈并入栈到栈2,再将栈2输出就得到了正常的队列输出
- 栈1:1 2 3 4 5; 栈2:
- 队列头部加入元素:
- 栈1出栈入栈2 --> 栈1: ; 栈2:5 4 3 2 1
- 栈1入栈新元素 --> 栈1:6; 栈2:5 4 3 2 1
- 栈2出栈入栈1 --> 栈1:6 1 2 3 4 5; 栈2:
- 此时执行队列输出时,将栈1的元素出栈并入栈到栈2,再将栈2输出就得到了正常的队列输出
代码
class QueBy2Stack {public:QueBy2Stack(){}void appendTail(int i_uNum);int deleteTail();void appendHead(int i_uNum);int deleteHead();void PrintQue();private:stack<int> s1;stack<int> s2; };void QueBy2Stack::appendHead(int i_uNum) {if (s2.empty()){while (!s1.empty()){s2.push(s1.top());s1.pop();}}s1.push(i_uNum);while (!s2.empty()){s1.push(s2.top());s2.pop();} }int QueBy2Stack::deleteHead() {if (s2.empty()){while (!s1.empty()){s2.push(s1.top());s1.pop();}}int result = s2.top();s2.pop();while (!s2.empty()){s1.push(s2.top());s2.pop();}return result; }void QueBy2Stack::appendTail(int i_uNum) {s1.push(i_uNum); }int QueBy2Stack::deleteTail() {int result = s1.top();s1.pop();return result; }void QueBy2Stack::PrintQue() {if (s2.empty()){while (!s1.empty()){s2.push(s1.top());s1.pop();}}while (!s2.empty()){cout << s2.top() << " ";s1.push(s2.top());s2.pop();} }void QueueBy2Stack() {int num = 0;int result = 0;cout << "**Func1: appendHead**" << endl;cout << "Please input num(stop when input -1): ";while (-1 != num){cin >> num;if (-1 != num){qbs.appendHead(num);}}cout << "now queque = [";qbs.PrintQue();cout << "]" << endl;cout << "**Func2: deleteHead**" << endl;result = qbs.deleteHead();cout << "delete num [" << result << "]]from head" << endl;cout << "now queque = [";qbs.PrintQue();cout << "]" << endl;cout << "**Func3: appendTail**" << endl;cout << "Please input num(stop when input -1): ";num = 0;while (-1 != num){cin >> num;if (-1 != num){qbs.appendTail(num);}}cout << "now queque = [";qbs.PrintQue();cout << "]" << endl;cout << "**Func4: deleteTail**" << endl;result = qbs.deleteTail();cout << "delete num [" << result << "]]from tail" << endl;cout << "now queque = [";qbs.PrintQue();cout << "]" << endl; }4 | 剑指Offer20:表示数值的字符串
题目
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
数值(按顺序)可以分成以下几个部分:
小数(按顺序)可以分成以下几个部分:
- 至少一位数字,后面跟着一个点 ‘.’
- 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
- 一个点 ‘.’ ,后面跟着至少一位数字
整数(按顺序)可以分成以下几个部分:
部分数值列举如下:
[“+100”, “5e2”, “-123”, “3.1416”, “-1E-16”, “0123”]
部分非数值列举如下:
[“12e”, “1a3.14”, “1.2.3”, “±5”, “12e+5.4”]
提示:
1 <= s.length <= 20
s 仅含英文字母(大写和小写),数字(0-9),加号 ‘+’ ,减号 ‘-’ ,空格 ’ ’ 或者点 ‘.’
思路
按照给定的规则进行遍历字符串,如果有不符合的项就判定为false,直到遍历完整个字符串
题目规则:
+ & -
- 正负号最多只能出现两次
- 正负号出现的位置只有两种
- 字符串首:如+10 -123
- eE后:如1e+10 2e-3
小数点
- 小数点最多只能出现一次
- 小数点出现的位置必须在eE之前,如1.2e9 1.3e-10
- 小数点前或后可以没有数字
e & E
- 指数符最多只能出现一次
- 指数符前必须为数字,后可接正负号,紧接着必须也是数字,如1e9 1e-9
字符串中除了数字以外,只能出现以上三种符号
代码
bool StringByInter(const string& i_cStr) {int pos = 0; // 起点下标bool flg = false; // 记录小数点是否出现while (' ' == i_cStr[pos]) // 读 前缀空格{pos++;}if ('+' == i_cStr[pos] || '-' == i_cStr[pos]) // 读 符号{pos++;}if ('.' == i_cStr[pos]) // 读 小数点{flg = true;pos++;}if ('9' < i_cStr[pos] || '0' > i_cStr[pos]) // Ee不能在首位{return false;}while ('9' >= i_cStr[pos] && '0' <= i_cStr[pos]) // 读 数字{pos++;}if ('.' == i_cStr[pos]) // 读 小数点{if (flg){return false; // 若已存在直接返回 false}else{pos++;while ('9' >= i_cStr[pos] && '0' <= i_cStr[pos]){pos++;}}}if ('e' == i_cStr[pos] || 'E' == i_cStr[pos]) // Ee的尾巴{pos++;if ('+' == i_cStr[pos] || '-' == i_cStr[pos]) // 读 符号{pos++;}if ('9' < i_cStr[pos] || '0' > i_cStr[pos]) // E 后面需要有数字{return false;}while ('9' >= i_cStr[pos] && '0' <= i_cStr[pos]){pos++;}}while (' ' == i_cStr[pos]) // 读后缀空格{pos++;}if ('\0' == i_cStr[pos]){return true;}else{return false;} }5 | 剑指Offer24:反转链表
题目
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
思路
对于一个1 2 3 4 5的链表,反转相当于把每一个节点的方向反向指向,如下
初始状态
1(head) -> 2 -> 3 -> 4 -> 5 -> NULL
反转状态
NULL <- 1 <- 2 <- 3 <- 4 <- 5(head)
双指针反转指向
从头结点开始,往后每个节点的指向都反转一边
定义两个指针
- cur:指向当前需要反转节点的指针
- pre:用于反转方向的指针
反转时定义临时指针
- tmp:用于存放下一个要处理的节点
反转的时候就是先保存下一个要处理的节点tmp,再把cur重新指向pre(这一步就已经实现了cur节点的反转),处理下一个之前,先把两指针后移到对应位置,cur移动到tmp位置,pre移动到cur的位置,图解如下
代码
ListNode* ReverseLink(ListNode* head) {ListNode* tmp;ListNode* pre = NULL;ListNode* cur = head;while (cur){tmp = cur->next;cur->next = pre;pre = cur;cur = tmp;}head = pre;return head; }6 | 剑指Offer30:包含min函数的栈
题目
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
思路
题目的关键在于三个操作的时间复杂度都是O(1),所以常规的遍历栈找到最小值返回的方法肯定不行
时间复杂度为O(1),说明执行min()函数跟栈中元素的多少无关,这一点其实也给出了思路,怎么定义minStack的数据结构,以便执行min()时可以直接拿到最小值,不需要其他多余的操作?
数据结构定义
通过两个栈定义这个minStack最小栈的数据结构
为什么不能定义一个变量来存放最小值,而定义一个栈呢?
如果使用一个变量来存放最小值的话,那么当弹出栈顶元素后,这个变量应该怎么随着元素的弹出进行变值很难确定,变量只能存放当前的值,对于之前的最小值没有记忆,所以这里采用两个栈,而不是一个栈搭配一个变量定义数据结构
函数定义
如果要以上三个元素的时间复杂度都是O(1),那么
- Normal:正常的压入弹出元素即可
- Mini:
- pop:正常弹出即可
- push:要判断当前要压入的元素是否比当前栈顶的元素小,如果是则压入,否则再次压入一个当前栈顶元素
为什么对于Mini这个栈的push操作需要这样?
目的是为了保证两个栈的元素同步性以及保证弹出压入元素时,不影响之前的Mini最小值。因为对于Mini来说:如果不进行比较直接压入的话,那么就失去了保存最小值的特性;如果比较后发现当前压入的元素比当前栈顶元素大就不压入元素的话,那么当弹出操作执行时,就会导致下一次的最小值错乱。
比如依次压入元素1 2 0 4 -1,那么Mini中的元素如下- 不比较压入:Mini = 1 2 0 4 -1,执行三次pop后,变为Mini = 1 2,这时候执行min返回的值就不对(2 > 1)
- 比较不压入:Mini = 1 0 -1,执行两次pop后,变为Mini = 1,这时执行min返回的值也不对(1 > 0)
- 比较压入:Mini = 1 1 0 0 -1,执行两次pop后,变为Mini = 1 1 0,这时执行min返回的值是正确的(0 == 0),再执行一次pop,变为Mini = 1 1,这时执行min返回的值也是正确的(1 == 1)
代码
class minStack {public:minStack(){}void minPush(int i_uNum);void minPop();int minTop();int minMin();private:stack<int> Normal;stack<int> Mini; };void minStack::minPush(int i_uNum) {Normal.push(i_uNum);if (!Mini.empty()){int num = i_uNum > Mini.top() ? Mini.top() : i_uNum;Mini.push(num);}else{Mini.push(i_uNum);} }void minStack::minPop() {if (!Normal.empty() && !Mini.empty()){Normal.pop();Mini.pop();}else{return;} }int minStack::minTop() {int res = -1;if (!Normal.empty()){res = Normal.top();}return res; }int minStack::minMin() {int res = -1;if (!Mini.empty()){res = Mini.top();}return res; }7 | 剑指Offer35:复杂链表的复制
题目
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
思路
利用哈希表的查询特点,考虑构建 原链表节点 和 新链表对应节点 的键值对映射关系,再遍历构建新链表各节点的next和random引用指向即可,大概思路如下
代码
class Node {public:int val;Node* next;Node* random;Node(int _val) {val = _val;next = NULL;random = NULL;} };Node* CopyRandomList(Node* head) {// 1. 如果链表为空返回空指针if (nullptr == head){return nullptr;}Node* cur = head;unordered_map<Node*, Node*> map;// 2. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射while(nullptr != cur) {map[cur] = new Node(cur->val);cur = cur->next;}cur = head;// 3. 构建新链表的 next 和 random 指向while(nullptr != cur){map[cur]->next = map[cur->next];map[cur]->random = map[cur->random];cur = cur->next;}// 4. 返回新链表的头节点return map[head]; }8 | 剑指Offer58:左旋转字符串
题目
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例1
- 输入: s = “abcdefg”, k = 2
- 输出: “cdefgab”
示例2
- 输入: s = “lrloseumgh”, k = 6
- 输出: “umghlrlose”
思路
两种方法:原地旋转 和 子串拼接
原地旋转
不需要额外申请空间,对原字符串进行旋转操作得到最终结果,流程如下
子串拼接
申请额外的空间存放结果,结果由[0, n) 和 [n, len)的两个子串拼接而得
代码
string ReverseLeftStr1(string& i_cStr, int i_uNum) {if (0 == i_uNum){return i_cStr;}else if (0 > i_uNum){i_uNum = -i_uNum;i_uNum = i_cStr.length() - i_uNum;}reverse(0, i_uNum);reverse(i_uNum, i_cStr.length());reverse(0, i_cStr.length());return i_cStr; }string ReverseLeftStr2(const string& i_cStr, int i_uNum) {string res = i_cStr;if (0 == i_uNum){return res;}else if (0 < i_uNum){res = res.substr(i_uNum) + res.substr(0, i_uNum);}else{i_uNum = -i_uNum;int len = res.length();res = res.substr(len - i_uNum) + res.substr(0, len - i_uNum);}return res; }9 | 剑指Offer59:滑动窗口的最大值
题目
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
示例
- 输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
- 输出: [3,3,5,5,6,7]
解释:
思路
暴力计算
设数组 nums的长度为 n,则共有 (n−k+1)(n - k + 1)(n−k+1) 个窗口;
获取每个窗口最大值需线性遍历,时间复杂度为 O(k)O(k)O(k)。
根据以上分析,则暴力法 的时间复杂度为 O(nk)O(nk)O(nk) 。
双端队列
设数组 nums的长度为 n,则共有 (n−k+1)(n - k + 1)(n−k+1) 个窗口;
获取每个窗口最大值需线性遍历,时间复杂度为 O(1)O(1)O(1)。
根据以上分析,则暴力法 的时间复杂度为 O(n)O(n)O(n) 。
代码
vector<int> MaxValSlidWin1(vector<int>& i_uArr, int i_uNum) {vector<int> res;if (0 == i_uArr.size() || 0 == i_uNum){return res;}int len = i_uArr.size();for (int i = 0; i <= len - i_uNum; i++){int max = i_uArr[i];for (int j = 1; j < i_uNum; j++){if (max < i_uArr[i + j]){max = i_uArr[i + j];}}res.push_back(max);}return res; }vector<int> MaxValSlidWin2(vector<int>& i_uArr, int i_uNum) {vector<int> res;deque<int> Maxi;if (0 == i_uArr.size() || 0 == i_uNum || 1 == i_uNum){return res;}Maxi.push_back(i_uArr[0]);for (int i = 1; i < i_uNum; i++){if (i_uArr[i] < Maxi.back()){Maxi.push_back(i_uArr[i]);}else{while (!Maxi.empty() && i_uArr[i] > Maxi.back()){Maxi.pop_back();}Maxi.push_back(i_uArr[i]);}}res.push_back(Maxi.front());for (int i = i_uNum; i < i_uArr.size(); i++){if (Maxi.size() == i_uNum) {Maxi.pop_front();}if (i_uArr[i] < Maxi.back()){Maxi.push_back(i_uArr[i]);}else{while (!Maxi.empty() && i_uArr[i] > Maxi.back()){Maxi.pop_back();}Maxi.push_back(i_uArr[i]);}res.push_back(Maxi.front());if (i_uArr[i - i_uNum + 1] == Maxi.front()){Maxi.pop_front();}}return res; }10 | 剑指Offer59:队列的最大值
题目
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
示例1:
- 输入:
- 输出:
示例 2:
- 输入:
- 输出:
思路
题目跟剑指Offer30:包含min函数的最小栈类似,大概思路与其一致,区别在于:
一样的,本题若只维护一个最大值变量,在元素入队时更新此变量即可;但当最大值出队后,并无法确定下一个 次最大值,因此不可行。所以也是通过构造数据结构来实现这个目的,与栈不同的是,这里数据结构申请的不是两个队列,而是一个单向队列和一个双向队列,双向队列用来保存队列中的最大值,双向队列随着单向队列的入队和出队操作实时更新,这样队列最大元素就始终对应双向队列的首元素。
初始化队列 queue ,双向队列 deque ;
-
最大值 max_Value() :
- 当双向队列 deque 为空,则返回 -1−1 ;
- 否则,返回 deque 首元素;
-
入队 push_Back() :
- 将元素 value 入队 queue ;
- 将双向队列中队尾 所有 小于 value 的元素弹出(以保持 deque 非单调递减),并将元素 value 入队 deque ;
-
出队 pop_Front() :
- 若队列 queue 为空,则直接返回 -1−1 ;
- 否则,将 queue 首元素出队;
- 若 deque 首元素和 queue 首元素 相等 ,则将 deque 首元素出队(以保持两队列 元素一致 );
例子
代码
class maxQue {public:maxQue(){}int max_Value();int pop_Front();void push_Back(int i_uNum);private:queue<int> Normal;deque<int> Maxi; };int maxQue::max_Value() {return Maxi.empty() ? -1 : Maxi.front(); }int maxQue::pop_Front() {int res = -1;if (Normal.empty()){return res;}res = Normal.front();if (Maxi.front() == res){Maxi.pop_front();}Normal.pop();return res; }void maxQue::push_Back(int i_uNum) {Normal.push(i_uNum);while (!Maxi.empty() && Maxi.back() < i_uNum){Maxi.pop_back();}Maxi.push_back(i_uNum); }11 | 剑指Offer67:把字符串转换成整数
题目
写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。
该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0。
说明:
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
示例 1:
- 输入: “42”
- 输出: 42
示例 2:
- 输入: " -42"
- 输出: -42
- 解释: 第一个非空白字符为 ‘-’, 它是一个负号。我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到-42 。
示例 3:
- 输入: “4193 with words”
- 输出: 4193
- 解释: 转换截止于数字 ‘3’ ,因为它的下一个字符不为数字。
示例 4:
- 输入: “words and 987”
- 输出: 0
- 解释: 第一个非空字符是 ‘w’, 但它不是数字或正、负号。
因此无法执行有效的转换。
示例 5:
- 输入: “-91283472332”
- 输出: -2147483648
- 解释: 数字 “-91283472332” 超过 32 位有符号整数范围。
因此返回 INT_MIN (−231) 。
思路
- true:遍历并判断后面是否为数字,直到遇到非数字字符停止,注意判断数字的范围(如果开头的数字是0,那么要跳过前置0,因为0123实际数字是123)
- false:直接返回0
需要申请一个long long类型的结果变量,否则无法判断,只会导致存放结果溢出,得到错误值
代码
int Str2Int(const string& i_cStr) {long long res = 0;if ("" == i_cStr){return (int)res;}int iStart = 0;int iLen = i_cStr.length();while (' ' == i_cStr[iStart]){iStart++;}if ((iLen == iStart) || (!IsInter(i_cStr[iStart]) && ('+' != i_cStr[iStart]) && ('-' != i_cStr[iStart]))){return (int)res;}bool Minus = false;if ('-' == i_cStr[iStart]){iStart++;Minus = true;}else if ('+' == i_cStr[iStart]){iStart++;}while ('0' == i_cStr[iStart]){i++;}while (IsInter(i_cStr[iStart])){res *= 10;res += i_cStr[iStart] - '0';iStart++;}bool Big = false;if (res > INT32_MAX){res = INT32_MAX;Big = true;}if (Minus){res = -res;if (Big){res = INT32_MIN;}}return (int)res; }总结
以上是生活随笔为你收集整理的【Code Pratice】—— 《图解算法数据结构 ~ 第一章》的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 触屏版canvas画布实现touch坐标
- 下一篇: Barracuda WSF v4.x -