代码的规范性
用完整的英文单词组合命名变量和函数
代码的完整性
- 基本功能:功能测试
- 边界测试
- 负面测试:不合规范的非法输入
3种错误的处理方法:
1、返回值:优点:和系统API一致,缺点:不方便使用计算结果
2、全局变量:优点:能够方便地使用计算结果,缺点:用户可能会忘记检查全局变量
3、异常:优点:可以为不同的出错原因定义不同异常类型,逻辑清晰明了 缺点:有些语言不支持异常,抛出异常对性能有负面影响
面试题11:数值的整数次方
题目:实现函数double Power(double base,int exponent),求base的exponent次方,不得使用库函数,同时不需要考虑大数问题
思路:
- 不需要考虑大数问题,数值的整数次方
- 指数为负,指数值取绝对值,算出次方结果取倒数。取导出,要考虑底数为0的情况;
- 边界情况,底数为0指数为负
- 边界检查情况:返回值,全局变量和throw pow函数一般会返回计算的值,使用全局变量记录错误
- 判断底数是不是0,不能直接base==0,计算机在表示小数时,都有误差。判断两个小数是否相等,只能判断它们之差是不是在一个很小的范围内,如果两数相差很小,可以认为他们相等
bool g_InvalidInput=false;
bool equal(double num1,double num2)
{
if((num1-num2)>-0.0000001&&(num1-num2)<0.0000001)
{
return true;
}
return false;
}
//优化前的
double PowerWithUnsignedExponent(double base,usignede int exponent)
{
double resulte=1.0; //exponent 小于0,等于0,大于0
for (int i=1;i<=absExponent;i++)
{
resulte*=base;
}
return result;
}
//优化后的
//已知16次方,16次方再平方就是32次方以此类推 递归调用 快速乘方
double PowerWithUnsignedExponent(double base,usignede int exponent)
{
if(exponent==0)
{
return 1.0;
}
if(exponent==1)
{
return base;
}
//右移运算符代替除以2,用位与运算符代替求余运算符来判断奇偶
double result=PowerWithUnsignedExponent(base,exponent>>1);
result*=result;
if(exponent&0x01==1)
result*=base;
return result;
}
double Power(double base,int exponent )
{
g_InvalidInput=false;
// 特殊情况处理 base小于0,等于0,大于0
if (equal(base,0.0)&&exponent<0)
{
g_InvalidInput=true;
return 0.0;
}
unsigned int absExponent=(usigned int)exponent;
if (exponent<0)
{
absExponent=(usigned int)(-exponent);
}
double result=PowerWithUnsignedExponent(base,absExponent);
if (exponent<0)
{
result=1.0/resulte;
}
return result;
}
面试题12:
题目:输入数字n,按顺序打印出从1最大的n位十进制数,比如输入3,则打印出1、2、3一直到最大数999
思路:
- 输入数字n,求最大数,3对应的最大数为999,在从1打印到999
- 但输入数字n是多大?其对应的最大值可能超过整数所能表达的范围
- 存在陷阱,大数问题
- 表达一个很大的数,用字符串或数组来表示,
- 字符串表达数字,最直观的方法,每个字符都是‘0’-‘9’,中的一个,用来表示数字中的一位,因为数字最大是n位,因此需要n+1位,最后一位为‘\0’表示结束,实际数字不够n位时,字符串的前半部分补0.
- 初始化字符串为‘0’,在字符串上模拟加法,把字符串表达的数字打印出来
//bool 返回值,是否递增到最大数
//递增,模拟加法,关键点,该位是否大于或等于10,是否要进1,
//以一个临时变量,记录目前该位的值
//该位没有大于10,直接赋值,跳出该循环
//该位大于10,该位减10,记录进位标志位
//继续处理下一个字符
//若第一位进1代表已经到达最大位
bool increment(char*number)
{
bool isOverFlow=false;
int nTakeOver=0;
int nLength=strlen(number);
for (int i=nLength-1;i>=0;i--)
{
int nSum=number[i]-'0'+nTakeOver;
if (i==nLength-1)
{
nSum++;
}
if (nSum>=10)
{
if (i==0)
{
isOverFlow=true;
}
else
{
nSum-=10;
nTakeOver=1;
number[i]='0'+nSum;
}
}
else
{
number[i]='0'+nSum;
break;
}
}
return isOverFlow;
}
//001 要打印为1而不是001
void printNumber(char*number)
{
bool isBeginning0=true;
int nLength=strlen(number);
for (int i=0;i<nLength;++i)
{
if (isBeginning0&&number[i]!='0')
{
isBeginning0=false;
}
if (!isBeginning0)
{
printf("%c",number[i]);
}
}
printf("\t");
}
//初始化数组为0,从1开始递增,直到最大数,打印出递增的数值;终止条件为递增到最大数
//递增,打印数值
void printItem(int n)
{
if(n<0)
return;
char*number=new char[n+1];
memset(number,'0',n);
number[n+1]='\0';
while(!increment(number))
{
printNumber(number);
}
delete [] number;
}
面试小提示:如果面试题是关于n位的整数并且没有限定n的取值范围,或者是输入任一大小的整数,那么这个题目很有可能要考虑大数问题
面试题13:给定单向链表的头指针和一个节点指针,定义一个函数在O(1)时间删除该节点,链表节点与函数定义如下
struct ListNode
{
int m_Value;
ListNode* m_pNext;
};
思路1:正常顺序从头节点遍历,查找到删除的节点,在链表中删除该节点,时间复杂度O(N) 不满足要求
思路2:给定了节点指针,删除该节点,知道节点的next,用节点next的内容覆盖当前节点,删除next节点,同样能达到效果,时间复杂度为O(1)
注意点:
特殊边界,只有一个头指针,头指针就是待删除的节点,直接删除;尾节点,无next,顺序查找到该节点,删除
总的时间复杂度((n-1)*O(1)+O(N))/n 最后的时间复杂度为O(1)
假设删除的节点存在,确定删除节点是否存在需要O(N)的复杂度,因此,把确定节点复杂的工作交给函数调用的人
void DeleteNode(ListNode**pListHead,ListNode*pToBeDeleted)
{
if (*pListHead==NULL||pToBeDeleted==NULL)
{
return;
}
if (pToBeDeleted->m_pNext!=NULL)
{//要删除的结点不是尾结点
ListNode*pNext=pToBeDeleted->m_pNext;
pToBeDeleted->m_Value=pNext->m_Value;
pToBeDeleted->m_pNext=pNext->m_pNext;
delete pNext;
pNext=NULL;
}
else if (*pListHead==pToBeDeleted)
{//链表只有一个结点,删除头节点,也是尾结点
delete pToBeDeleted;
pToBeDeleted=NULL;
*pListHead=NULL;
}
else
{//链表中有多个结点,删除尾结点
ListNode*pNode=*pListHead;
while(pNode->m_pNext!=pToBeDeleted)
{
pNode=pNode->m_pNext;
}
pNode->m_pNext=NULL;
delete pToBeDeleted;
pToBeDeleted=NULL;
}
}
面试题14:
题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分
思路1:奇数在前,偶数在后,不考虑顺序,遇到偶数,后面的往前挪,放在数组末尾 时间复杂度O(N^2) 不满足要求
思路2:奇数放在前半组,偶数放在后半组,混合到分组,可以将前面偶数和后面奇数互换,维护2个指针,往中间走
拓展://方法的可扩展性:1、判断数字在数组前半部分还是否后半部分的标准 2、拆分数组的操作
举一反三:双指针 一前一后
void ReOrderOddEven(int*pData,unsigned int len)
{
if (pData==NULL||len==0)
{
return;
}
int*pBegin=pData;
int*pEnd=pData+len-1;
while(pBegin<pEnd)
{
while (pBegin<pEnd&&(*pBegin&0x01)!=0)
{//前为奇数
pBegin++;
}
while (pEnd>pBegin&&(*pEnd&0x01)==0)
{//后为偶数
pEnd--;
}
if (pBegin<pEnd)
{
int temp=*pBegin;
*pBegin=*pEnd;
*pEnd=temp;
}
}
}
面试题15:输入一个链表,输出该链表中倒数第K个结点,为了符合大多数人的习惯,本题从1开始计数,即链表的尾结点是倒数第1个结点,例如
一个链表有6个结点,从头开始它们的值依次是1,2,3,4,5,6 这个链表的倒数第3个结点是4
思路1:链表依次遍历,到结尾,再回溯K个结点 但链表尾单链表,无法回溯不能实现
思路2:链表只能依次遍历,查找倒数第K个结点,倒数K个结点即为正数第N-K+1个结点。K已知,N未知,先第一次遍历统计总的结点为N,再第2次遍历到N-K+1个结点。思路2实现了要求,但是遍历了两次,效率太低
思路3:双指针,倒数第K个结点,pBehind先走K-1步,pAhead指向头结点,然后两个指针一起往前走,pBehind指向尾结点,pAhead指向倒数第K个结点
注意点:程序的鲁棒性:输入数据的合理性检查:pHead指针是否为空,K值是否合理(是否为0,或者大于链表结点数)
举一反三:当我们用一个指针遍历链表不能解决问题的时候,可以尝试用两个指针来遍历链表,可以让其中一个指针遍历的速度快一些(比如一次走2步)或者让它先在链表上走若干步。
struct ListNode
{
int m_nValue;
ListNode*m_pNext;
};
ListNode* FindKthToTail(ListNode* pHead,unsigned int k)
{
//输入数据的合理性检查
if (pHead==NULL||k==0)
{
return;
}
ListNode*pAhead=pHead;
ListNode*pBehind=pHead;
for(unsigned int i=0;i<k-1;i++)
{
//K值大于链表结点数 没有到结尾,pAhead->m_pNext为空
if (pAhead->m_pNext==NULL)
{
return NULL;
}
else
{
pAhead=pAhead->m_pNext;
}
}
while (pAhead->m_pNext!=NULL)
{
pAhead=pAhead->m_pNext;
pBehind=pBehind->m_pNext;
}
return pBehind;
}
面试题16:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。
注意点:链表题目,全面分析,写出鲁棒性较好的代码,实际应聘中:1、输入的链表头指针为NULL或者整个链表只有1个结点,程序立即崩溃 2、反转后的链表出现断裂 3、返回反转之后的头结点不是原始链表的尾结点 。测试用例:输入的链表头指针为NULL 输入的链表只有一个结点 输入的链表有多个结点
思路:正确的反转链表,需要调整链表中指针的方向,调整指针的方向,一个指针指向当前需要调整的结点i,一个指针指向调整结点的前一个节点j,为避免链表在节点i处断开,需要在调整i之前将i的m_pNext保存下来,pPrev pCur pNext,反转后 链表的头结点是原始链表的尾结点。
ListNode* ReverseList(ListNode*pHead)
{
// ListNode*pPrev=NULL;
// ListNode*pCur=pHead;
// ListNode*pRverseHead=NULL;
// while (pCur)
// {
// ListNode*pNext=pCur->m_pNext;
// if (pNext==NULL)
// {
// pRverseHead=pCur;
// }
// pCur->m_pNext=pPrev;
//
// pPrev=pCur;
// pCur=pNext;
// }
//
// return pRverseHead;
//递归
//递归的终止条件
if(pHead->m_pNext==NULL||pHead==NULL)
return pHead;
ListNode*node=ReverseList(pHead->m_pNext);
pHead->m_pNext->m_pNext=pHead;
pHead->m_pNext=NULL;
return node;
}
面试题17;
题目:输入两个递增排序的链表,合并这个两个链表并使新链表中的结点仍然是按照递增排序的
思路:1、链表递增,使用一个指针穿针引线,链表1的结点和链表2的结点值比较,指向结点值较小的结点,并移动该结点所在的链表指针。设置dummyhead,保存新的链表头结点。
2、递归,递归结束的点为传入的指针为空,递归进行的点,比较两个链表的节点值,调整指针的指向,递归调用。
代码
struct ListNode
{
int m_nValue;
ListNode*m_pNext;
};
ListNode* MergeTwoList(ListNode*list1,ListNode*list2)
{
//递归
//递归结束的条件
// if (list1==NULL)
// {
// return list2;
// }
// if (list2==NULL)
// {
// return list1;
// }
//
// ListNode*pMergehead=NULL;
// if (list1->m_nValue<list2->m_nValue)
// {
// pMergehead=list1;
// pMergehead->m_pNext=MergeTwoList(list1->m_pNext,list2);
// }
// else
// {
// pMergehead=list2;
// pMergehead->m_pNext=MergeTwoList(list1,list2->m_pNext);
// }
//
// return pMergehead;
if (!list1)
{
return list2;
}
if (list2)
{
return list1;
}
//构建哑巴头结点保存新的头结点, 局部变量穿针引线穿起来,根据大小值的比较
//鲁棒性:头结点为NULL的处理
ListNode*dummyHead=new ListNode(),*pre=dummyHead;
while (!list1&&!list2)
{
if (list1->m_nValue>list2->m_nValue)
{
pre->m_pNext=list2;
pre=pre->m_pNext;
list2=list2->m_pNext;
}
else
{
pre->m_pNext=list1;
pre=pre->m_pNext;
list1=list1->m_pNext;
}
}
if (!list1)
{
pre->m_pNext=list1;
}
if (!list2)
{
pre->m_pNext=list2;
}
return dummyHead;
}
面试题18
题目:输入两棵二叉树A和B,判断B是不是A的子结构。
思路:查找树A和树B是否存在一样的子结构
第一步,在树A中找到和B的根结点的值一样的结点R
第二步:判断树A中以R为根结点的子树是不是包含和树B一样的结构
代码:
struct BinaryTreeNode
{
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
};
//根结点相同,判断两个子树是否相同,如果结点R的值和树B的根结点不相同,则以R为根结点的子树与树B肯定不具有相同的结点,如果
//它们的值相同,则递归地判断他们各自的左右结点值是否相同,递归的终止条件是我们到达了树A或者树B的叶结点。
bool DoesTree1HvaeTree2(BinaryTreeNode*pRoot1,BinaryTreeNode*pRoot2)
{
if (pRoot1==NULL)
{
return false;
}
if (pRoot2==NULL)
{
return true;
}
if (pRoot1->m_nValue!=pRoot2->m_nValue)
{
return false;
}
return DoesTree1HvaeTree2(pRoot1->m_pLeft,pRoot2->m_pLeft)&&DoesTree1HvaeTree2(pRoot1->m_pRight,pRoot2->m_pRight);
}
//在结点指针都不为NULL(判断边界条件,检查空指针)时,先找到根结点相同的位置
//根结点不同,继续找根结点相同的结点
bool HasSubTree(BinaryTreeNode*pRoot1,BinaryTreeNode*pRoot2)
{
bool result=false;
if (pRoot1!=NULL&&pRoot2!=NULL)
{
if (pRoot1->m_nValue==pRoot2->m_nValue)
{
result=DoesTree1HvaeTree2(pRoot1,pRoot2);
}
if(!result)
result=HasSubTree(pRoot1->m_pLeft,pRoot2);
if (!result)
result=HasSubTree(pRoot2->m_pRight,pRoot2);
}
return result;
}
总结:
面试写出高质量的代码:规范性(书写清晰、布局清晰、命名合理)、完整性(完成基本功能、考虑边界条件、做好错误处理)、鲁棒性(采取防御式编程、处理无效输入)