问题: 1. 在项目中遇到的最大的问题是什么,怎么解决的? 2. 从项目中学到了什么 3. 等等
问: 定义一个空的类型,求sizeof的结果。 答:通常为1(或0)取决于编译器。因为对象必须占用一定的空间。 问:如果内定义函数呢? 答:除了虚函数会在每个实例中添加一个指向虚函数表的指针外,其他函数不占有空间。 附:类的方法,是通过this指针实现的。除了虚方法是通过每个类自带的虚函数表指针来实现的。
相关注意点: 1. 返回:构造无返回;赋值返回本身引用。 2. 传入形式:通常为常量引用。尤其是构造函数内,如果非引用,则由实参构造形参的时候,就会需要递归调用构造函数,引发死循环。 3. 是否需要释放内存:常见于含有指针的类。赋值运算符结合了析构和构造,要释放和重新分配内存。 4. 自我赋值:自我赋值时候常常因为释放内存产生问题。
class CMyString{ public: CMyString(char* pData=NULL); CMyString(const CMyString& str); ~CMyString(); private: char* pData; };一般写法
CMyString& CMyString::operator= (const CMyString& str){ if(str==*this) return *this; delete[] pData; pData=new char[strlen(str.pData)+1]; strcpy(str.pData,pData); return *this; }带有异常保护的写法 如果内存分配异常,则不应先delete内存,这样异常发生后现场无法恢复。 所以应该先分配,再delete。以下用了一个很高明的方法: 1. 创建临时对象。则该对象中已经分配好了内存,并拷贝了数据。 2. 交换该对象的指针与类成员的指针。这样类成员指针成功接管了数据区,完成了新数据的纳入;临时对象的指针接管了旧数据区,会在函数结束时析构,完成了旧数据的释放。
CMyString& CMyString::operator= (const CMyString& str){ if(str!=*this){ CMyString temp(str); swap(temp.pData,pData); } return *this; }vector当尺寸size超过容量capacity时候,会内存重分配,重新申请当前两倍的容量。 数组是内存上一块连续的区域,指针是内存上一个位置。 数组名可以当做指针来使用,通常在作为右值的场合,比如使用数组对指针作赋值和初始化,函数参数传入,返回值传出等。 例外:使用引用作赋值和初始化,使用引用作函数传入传出,sizeof typeid等运算符等。 值得注意的是,函数内形参即使是数组的形式,本质也是指针。
题目:在一个二维数组中,每一行都是从左向右递增,每一列都是从上到下递增。设计函数,判断该矩阵是否含有某个值。 1 2 8 9 2 4 9 12 4 7 10 13 6 8 11 15 设定一个起始点,如果开始在左下角位置6,如果目标值大于6,目标必然不会在第一列中,所以向右移动到8.这样搜索区域从以6作为左下角的4x4矩阵变成了以8为左下角的4x3矩阵。 所以规则如下: 1. 起始点在左下角 2. 当前值等于目标,目标已找到,函数返回 3. 目标大于当前值,则右移,否则左移。 4. 若移动出界,则查找失败。
bool Find(int target, vector<vector<int> > array) { if(array.empty()||array[0].empty()) return false; int rows=array.size(), cols=array[0].size(); int r=rows-1,c=0; while(1){ if(target==array[r][c]){ return true; } if(target>array[r][c]){ c++; }else{ r--; } if(c>=cols || r<0){ return false; } } return false; }常量字符串存储于字符串常量区。即,相同的常量字符串地址相同。
题目:请实现一个函数,将一个字符串中的空格替换成“ ”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We Are Happy。 从头到尾的插入,会大大消耗时间,所以使用从尾到头的誊写。 誊写前,先确定最终长度。注意新串的结束符。然后用两个指针分别指向原串尾和新串尾。
void replaceSpace(char *str,int length) { int num0=0; for(int i=0;i<length;++i){ if(str[i]==' ') num0++; } int newLen=length+2*num0; str[newLen]='\0'; //一定要注意 for(int i=length-1,j=newLen-1;i>=0;){ if(str[i]!=' '){ str[j--]=str[i--]; }else{ str[j--]='0'; str[j--]='2'; str[j--]='%'; i--; } } }链表元素的插入和删除要影响前驱节点,所以迭代的对象,一定是前驱节点。 无头链表要特别注意空指针和头节点的处理。
题目:输入一个链表,从尾到头打印链表每个节点的值。不许使用额外O(N)的空间。
void reverPrint(ListNode* p){ if(p==nullptr){ return; } rever(p->next); cout<<p->val; }题目:输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 利用中序序列中,根节点会划分序列为左右子树的特性;找到根节点,划分左右子树,并对左右分别递归调用函数即可。
前:1 2 4 5 3 6 7 中:4 2 5 1 6 3 7 根节点:1 劈开中序: 4 2 5 + 6 3 7 依次匹配前序:2 4 5 + 3 6 7 继续递归调用。
TreeNode* reBuild(int* f,int* m,int n){ if(n<=0)//终止条件,千万别忘了 return nullptr; TreeNode* p=new TreeNode(f[0]); int i=0; for(i=0;i<n;++i){ if(m[i]==f[0]) break; } p->left=reBuild(f+1,m,i); p->right=reBuild(f+i+1,m+i+1,n-i-1); return p; }思路:两个栈IN和OUT 入队时直接压入IN栈; 出队时若OUT栈为空,则从IN依次弹出并压入OUT内。从OUT弹出元素。
class Solution { public: void push(int node) { stack1.push(node); } int pop() { if(stack2.empty()){ while(!stack1.empty()){ int node=stack1.top(); stack1.pop(); stack2.push(node); } } int node=stack2.top(); stack2.pop(); return node; } private: stack<int> stack1; stack<int> stack2; };扩展:用队列实现栈操作。 压栈:直接入队尾。 出栈:需要把队尾元素输送至队首。即队首依次弹出N-1个元素入队尾。然后弹出队首,即为出栈操作。 出栈后,其他元素依然按照原序排列,故无后效性。 注:也可以用另一个队列来辅助操作。
如果right=mid-1以及left=mid+1,那么最后两者会相遇。但如果没有加减一,最后两者会相邻。
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
int find(int* a,int n){ if(a[0]<a[n-1]){//原来头部较小,旋转后,较小的一定在后面。 return a[0]; } int left=0,right=n-1; while(left+1<right){ int mid=left+(right-left)/2; if(a[mid]>a[left]){ left=mid; }else if(a[mid]<a[left]){ right=mid; }else{//如果mid和left相等,无法确定在哪里。例如:3 3 1 2 3 for(int i=left;i<right;++i){ if(a[i]>a[i+1]){ return a[i+1]; } } } } //使用非缩减mid的好处,就是结果一定在left和right两者之间。 return a[left]<a[right]?a[left]:a[right]; }一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
int jumpFloor(int number) { vector<int> s(number+1); s[1]=1; s[2]=2; for(int i=3;i<=number;++i) s[i]=s[i-1]+s[i-2]; return s[number]; }一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
int jumpFloorII(int number) { vector<long> s(number+1); s[1]=1; for(int i=2;i<=number;++i) s[i]=s[i-1]*2; return s[number]; }我们可以用2x1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2xn的大矩形,总共有多少种方法? 其实简单画一画,就看得出,递推公式很接近斐波那契。
int rectCover(int number) { vector<int> s(number+1); s[1]=1; s[2]=2; for(int i=3;i<=number;++i){ s[i]=s[i-1]+s[i-2]; } return s[number]; }对于有符号数,切记右移的时候,会填充符号位。所以位运算操作的往往是无符号数。 顺便提及一下STL里面bitset的使用:
bitset<n> b(u) b是unsigned long型u的一个副本,共n位,n为常数 bitset<n> b(s) b是string对象s中含有的位串的副本,共n位,n为常数 b.any() b中有1吗? b.none() b中全0吗? b.count() b中置为1的二进制位的个数 b.size() b中二进制位的个数 b[pos] 访问b中在pos处的二进制位 b.set() 把b中所有二进制位都置为1 b.set(pos) 把b中在pos处的二进制位置为1 b.reset() 把b中所有二进制位都置为0 b.reset(pos) 把b中在pos处的二进制位置为0 b.flip() 把b中所有二进制位逐位取反 b.flip(pos)把b中在pos处的二进制位取反 b.to_ulong()用b中同样的二进制位返回一个unsigned long值 os << b 把b中的位集输出到os流注意:如果把一个有符号数转换为无符号数,二进制是不会变的,它之后被无符号数的形式来解析。 日常处理的时候,除了右移之外,有无符号数位操作基本一致。
n=n&(n-1)会使得最右侧的1变为0. 例如:
n =xxxx1 n-1 =xxxx0 ans =xxxx0 n =xxxx1000 n-1 =xxxx0111 ans =xxxx0000关于错误的处理: 1. 返回值 2. 全局变量 3. 异常
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 基于鲁棒性的考虑: 1. 指数为0或负值的情况; 2. 底数为0的情况; 基于运算效率的考虑: B2n=Bn∗Bn 这样求B的N次方,就可以在O(logN)内求出。 我们把指数表示成二进制,从右向左遍历。如果某一位为1,则使得sum*=base。每次遍历base*=base. 例如3^5, 5= 0101 则结果为 1∗31+0∗32+1∗34+0∗38
double Power(double base, int exponent) { bool negE=false; if(exponent<0){ negE=true; exponent=-exponent; } bool zeroB=false; if(base>-0.00001 && base<0.00001){ zeroB=true; } if(zeroB && negE){ return -1;//一般抛出异常 } double sum=1; unsigned int ee=exponent; while(ee){ int last=ee&0x01; ee>>=1; if(last){ sum*=base; } base*=base; } if(negE){ sum=1.0/sum; } return sum; }其中的要点: 1. 指数为负的结果最后须求倒数。0的负指数无意义。0的0次方可以为0或1. 2. 两个浮点数判相等,通常看它们的差,是否在0的一个领域。因为浮点是近似表示的。
bool equal(double a,double b){ return (a-b>-0.00001 && a-b<0.00001); } //整数加减如果考虑越界:通常越界的结果是很大的值,所以当然不在0的领域内了。大数陷阱:如果单单指定n位数,很容易超过范围。这种类型一般用字符串或数组求解。 思路:n位数的全枚举。不足n位的,前面用0补齐。后续打印时注意即可。 全枚举问题用递归来完成。 递归求全枚举的基本框架是:分别枚举第m位置的所有可能,分别进入m+1个位置的递归。
void reOrder1(char* a,int m,vector<string >& res){ if(a[m]=='\0'){ res.push_back(a); return ; } for(int i=0;i<=9;++i){ a[m]='0'+i; reOrder1(a,m+1,res); } }全排列 递归求全排列的基本框架是:分别交换第m位置和m及其后的所有位置,分别进入m+1个位置的递归,然后交换回来。交换回来的步骤,就是回溯。回溯时恢复函数调用前的状态。
void reOrder2(char* a,int m,vector<string >& res){ if(a[m]=='\0'){ res.push_back(a); return; } for(int i=m;a[i]!='\0';++i){ swap(a[m],a[i]); reOrder2(a,m+1,res); swap(a[m],a[i]); } }具体代码未验证,此时暂略。
删除链表节点势必会更改其前驱。这里使用拷贝其后继节点,然后删除其后继节点来模拟。然而,这样不能删除尾节点;不能更改const节点;割裂了地址和内容的一致性。
输入一个整数数组,实现一个函数来调整数组,使得所有的奇数位于偶数前面,并保证奇数和奇数,偶数和偶数之间的相对位置不变。 如果没有相对位置的限制,可以使用快排的划分思想。但是快排是不稳定排序(即使没有随机交换过程,新旧都是)。 归并排序是稳定的,堆和快排和希尔都是不稳定的。 要想保证原有次序,则只能顺次移动或相邻交换。 遍历数组,如果当前是奇数,利用插入排序的思想,插入到前一个奇数的后面。 定义第0个奇数的位置是-1.
void reOrderArray(vector<int> &array) { int last=-1; for(int i=0;i<array.size();++i){ if(array[i]%2==1){ int get=array[i]; for(int j=i;j>last;--j){ array[j]=array[j-1]; } array[last+1]=get; last+=1; } } }关于快速划分:快速划分可以划分为两块,甚至是三块。三块的情形,就是在数组头尾分别设置区间,对遍历到的元素进行判断,来决定交换并纳入哪一个区间中。注意新交换过来的陌生元素还需要再处理一遍。
链表只有单向性,所以通常用快慢指针来快速查找某些位置。 1 2 3 4 5 6 7 空 有两种策略: 1. 当fast指向尾元素时,slow指向倒数第K个元素,则两指针相差K-1,所以开始阶段,fast前进K-1步。此后两指针同时移动,至fast为空。 2. 当fast为空时,slow指向倒数第K个元素,则两针相差K,开始阶段fast前进K步。此后两指针同时移动,至fast的next为空。
前者处理起来更容易,因为前者的开始阶段,fast始终是有效的,只需对其有效性进行检查,即可发现数组长度不足K的情况;后者的fast可能在前进最后一步时为空,检查起来略显麻烦。
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) { if(pListHead==nullptr) return nullptr; ListNode* fast=pListHead,*slow=pListHead; for(int i=0;i<k-1;++i){ fast=fast->next; if(fast==nullptr){ return nullptr; } } while(fast->next){ fast=fast->next; slow=slow->next; } return slow; }1 2 3 4 5 1 2 3 4 5 6 fast和slow初始都指向头,fast每次走两步,slow每次走一步,直至fast不存在下个节点或不存在下下个节点。此时,如果fast有下个节点,则证明是偶数长度;反之为奇数长度。此时,若是奇数,则slow在正中;若是偶数,则正中偏左。
fast和slow初始都指向头,fast每次走两步,slow每次走一步,直至fast为空或两指针相遇。前者证明无环,后者证明有环。 相遇后,fast不动,slow重新指向头; 接下来两者每次都走一步,直至相遇。相遇处即为入环点。
head->1->2->3->4->NULL 假设当前处理节点1,那么应该是求出其后的链表翻转后的尾节点,再把节点1添加到它的后面。这样就可以递归调用了: 递归函数输入:链表头指针; 输出:反转后的新头节点 注意:需要在主调函数里检查头指针有效性,以及手动添加尾节点的next为空。
ListNode* rever(ListNode* p){ if(p->next==nullptr){ return p; } //先反转后面的,再链接前面的,千万别反了 ListNode* newh=rever(p->next); p->next->next=p; return newh; } ListNode* ReverseList(ListNode* pHead) { if(!pHead) return nullptr; ListNode* newH=rever(pHead); pHead->next=nullptr; return newH; }如果拿循环来做,通常用3个指针:
ListNode* Rever(ListNode* head){ ListNode* newH,*p=head,*plast=nullptr; while(p){ ListNode* pnxt=p->next; if(pnxt==nullptr) newH=p; //最后三句首尾相连 p->next=plast; plast=p; p=pnxt; } return newH; }带头链表用循环好做,无头链表用递归好做。
在这里判断B是否是A的子结构,而非完整的子树,不能用KMP算法来做。 ps:我们约定空树不是任意一个树的子结构
8 8 / \ / \ 8 7 9 2 / \ 9 2重点在于匹配策略: 如果根节点不相等,则分别用A的左右子树,去和B匹配。两者任意一个匹配上,即匹配成功。 如果根节点相等,则分别用A的左右子树,去和B的左右子树匹配。两者均匹配,则匹配成功。 如果左右子树中有任何一个不匹配呢?匹配失败了吗? 不对,此时依然可以用A的左右子树,去和B匹配。
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) { if(pRoot1==nullptr || pRoot2==nullptr ) return false; if(pRoot1->val!=pRoot2->val){ return ( HasSubtree(pRoot1->left,pRoot2)|| HasSubtree(pRoot1->right,pRoot2) ); }else{ bool left=true,right=true; if(pRoot2->left) left=HasSubtree(pRoot1->left,pRoot2->left); if(pRoot2->right) right=HasSubtree(pRoot1->right,pRoot2->right); if(!(left &&right) ){ return ( HasSubtree(pRoot1->left,pRoot2)|| HasSubtree(pRoot1->right,pRoot2) ); }else{ return true; } } }在这里,规定空树不是任何树的子树。其实如果规定:如果B是A的子树,那么B的左右子树,分别和A的左右子树,依然构成子树包含关系。那么空树,应该是任意树的子树才对。 上文因为顾忌子树为空的问题,不能让B树为空,所以当B有左右孩子时,才能进入左右孩子的递归过程。如果定义空树为任意树的子树,那么代码可以更简单:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) { if(pRoot1==nullptr) return false; if(pRoot2==nullptr) return true; if(pRoot1->val!=pRoot2->val){ return ( HasSubtree(pRoot1->left,pRoot2)|| HasSubtree(pRoot1->right,pRoot2) ); }else{ bool left=true,right=true; left=HasSubtree(pRoot1->left,pRoot2->left); right=HasSubtree(pRoot1->right,pRoot2->right); if(!(left &&right) ){ return ( HasSubtree(pRoot1->left,pRoot2)|| HasSubtree(pRoot1->right,pRoot2) ); }else{ return true; } } }