剑指offer-第3章

代码的规范性

完整的英文单词组合命名变量和函数

代码的完整性

  1. 基本功能:功能测试
  2. 边界测试
  3. 负面测试:不合规范的非法输入

3种错误的处理方法
1、返回值:优点:和系统API一致,缺点:不方便使用计算结果
2、全局变量:优点:能够方便地使用计算结果,缺点:用户可能会忘记检查全局变量
3、异常:优点:可以为不同的出错原因定义不同异常类型,逻辑清晰明了 缺点:有些语言不支持异常,抛出异常对性能有负面影响
面试题11:数值的整数次方
题目:实现函数double Power(double base,int exponent),求base的exponent次方,不得使用库函数,同时不需要考虑大数问题

思路

  1. 不需要考虑大数问题,数值的整数次方
  2. 指数为负,指数值取绝对值,算出次方结果取倒数。取导出,要考虑底数为0的情况;
  3. 边界情况,底数为0指数为负
  4. 边界检查情况:返回值,全局变量和throw pow函数一般会返回计算的值,使用全局变量记录错误
  5. 判断底数是不是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
思路

  1. 输入数字n,求最大数,3对应的最大数为999,在从1打印到999
  2. 但输入数字n是多大?其对应的最大值可能超过整数所能表达的范围
  3. 存在陷阱,大数问题
  4. 表达一个很大的数,用字符串或数组来表示,
  5. 字符串表达数字,最直观的方法,每个字符都是‘0’-‘9’,中的一个,用来表示数字中的一位,因为数字最大是n位,因此需要n+1位,最后一位为‘\0’表示结束,实际数字不够n位时,字符串的前半部分补0.
  6. 初始化字符串为‘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;
}

总结
面试写出高质量的代码:规范性(书写清晰、布局清晰、命名合理)、完整性(完成基本功能、考虑边界条件、做好错误处理)、鲁棒性(采取防御式编程、处理无效输入)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值