《剑指Offer》读书笔记(2)——第二章 编程语言

前言

这是第二章,这章的内容相对第一章会充实很多,这章的内容主要讲解一些c++编程语言所应该注意的一些知识。

正文

经验总结

  1. C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型 生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数在的指针. 在 32 位的机器上,一个指针占 4 字节的空间,因此求 sizeof得到 4; 如果 是 64 位的机器,一个指针占 8 字节的空间,因此求取sizeof得到 8.
  2. 栈是一个与递归紧密相关的数据结构,同样队列也与广度优先遍历算法紧密相关。
  3. 数组可以用来实现哈希表,把数组的下标设为哈希表的键值(key),而把数字中的每一个数字设为哈希表的值(Value)。关于面试题中的“第一个中只出现一次的字母。”
  4. STL 的 vector 每次扩充容量时,新的容量都是前 一次的两倍。把之前的数据复制到新的数组之中,然后再次释放之前那个旧的数组。
  5. 当数组作为函数的参数进行传递时,数组就自动退化为同类型的指针。 因此尽管函数 GetSize 的参数 data被声明为数组,但它会退化为指针, size3 的结果仍然是 4。
  6. C/C++ 中每个字符E辑部以字符喻’作为结尾, 这样我们就能很方便地找 到字符串的最后尾部.
  7. 单向链表的节点定义:
struct ListNode{
	int m_nValue;
	ListNode *m_pNext;
}
  1. 注意二叉搜索树的特点:

在二叉搜索树中, 左子 结点总是小于或等于根结点,而右子结点总是大于或等于根结点。
平均查找时间为O(logn).

  1. 红黑树的定义:

红黑树是把树中的结点定 义为红、黑两种颜色,并通过规则确保从根结点到叶结点的最长路径的长 度不超过最短路径的两倍。

  1. 如果是在面试的时候要求查找某个数字或是统计某个数字出现的次数,我可以使用二分查找,这样的时间复杂度为O(logn).
  2. 面试宫会经常要求应聘者比较插入排序、冒 泡排序、归并排序、快速排序等不同算法的优劣。强烈建议应聘者在准备 面试的时候, 一定要对各种排序算法的特点烂熟于胸。
  3. 递归的每一次函数调用,都需 要在内存校中分配空间以保存参数、返回地址及临时变量,而且往栈里压入数据和弹出数据都需要时间。所以,有可能出现栈溢出的情况。
  4. 在输入数值进行判断的时候,一定要进行判断是否0,是否大于0.是否是负数。
  5. 拷贝构造函数的形参一定要是 A(const A& other) 不能是A(A other),因为后者由于是传值参数,会在形参赋值到实参的过程中调用赋值构造函数。如果允许复制构造函数传值的话,那么就会无休止的递归调用从而导致堆栈溢出。

面试题

在C++中,有哪4个与类型转换相关的关键字?

  1. reinterpret_cast<type_id>(expression)

reinterpret_cast是C++里面的一个强制类型转换符,能够将任何的指针类型转换成其他的任何指针类型;能够将任何的整数类型转换成指针类型,反之亦然;滥用reinterpret_cast强制类型转换符不安全。除非要转换成的类型是固有的低级别的,不然要考虑使用其他的转换操作符。

type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。

a、reinterpret_cast强制类型转换符能够将字符串类型的指针转换成整型指针(char * -> int *),某个类类型的指针转换成另外一个不相关类类型的指针(classA * -> class B *);这些转换不安全。
b、使用reinterpret_cast强制转换之后的结果很不安全,除非用来转换成它的原始类型。
c、reinterpret_cast不能用来移除变量的const、volatile等属性。
d、reinterpret_cast可以将一个空类型的指针转换成一个目标类型的空指针值。

总结:可以将一种指针类型转换成其他任意一种的指针类型,类似于可以将字符类型的指针转换为整数类型的指针,但这种转换很不安全,因为存在各种因为指针导致越界。比如你将int转成float,那么有可能去到的地方是超出Int的整数表示范围的。当你将指针转化为float后他就读出1的四个字节及其后面的额外四个字节 所以肯定不是1了,至于会是什么 应该是随机的。

  1. static_cast(type_id)(expression)

a、将一个基类的指针转换成派生类的指针,但是这种转换经常很不安全。但是将一个基类指针转换成基类指针或将派生类指针转换成派生类指针是安全的。用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。进行上行转换(把派生类的指针或引用转换成基类表示)是安全;进行下行转换(把基类指针或引用转换成派生类表示时)由于没有动态类型检查,所以是不安全的。

static_cast和reinterpret_cast的区别主要在于多重继承。C++primer第五章里写了编译器隐式执行任何类型转换都可由static_cast显示完成;reinterpret_cast通常为操作数的位模式提供较低层的重新解释。

static_cast不能用来移除变量的const、volatile等属性。

总结:static_cast这个与reinterpret_cast的差别主要就在于多重继承。在进行基类与基类的指针间或是子类与子类的指针间 的类型转换是推荐使用static_cast的。

  1. const_cast<type_id>(expression)

const_cast可以用于移除类型const、volatile等属性。该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外,type_id和expression的类型是一样的。

总结:这个数据类型与前面两个的差别就在于这个是可以用来移除类型中的const和volatile类型的。

  1. dynamic_cast<type_id>(expression)

a、如果type_id是一个指针类型,那么expression也必须是一个指针类型,如果type_id是一个引用类型,那么expression也必须是一个引用类型。

b、如果type_id是一个空类型的指针,在运行的时候,就会检测expression的实际类型,结果是一个由expression决定的指针类型。

c、如果type_id不是空类型的指针,在运行的时候指向expression对象的指针能否可以转换成type_id类型的指针。

d、在运行的时候决定真正的类型,如果向下转换是安全的,就返回一个转换后的指针,若不安全,则返回一个空指针。

总结:这个在转换时对类型的要求就较为严格了,要是type_id是指针的话,那么expression也应该是指针。

C++中可以用 struct 和 class 来定义类型. 这两种类型有什么 区别?

总结:若没有标明成员函数或者成员变量的访问权限级别,struct默认的是public。而class默认的是private。

用几行代码定义一个单例模式

不好的方法一:只适用于单线程环境

public class Singleton1
{
	private Singleton1()//将构造函数的访问权限级别设置为private,防止他们访问
	{
	}
	private static Singleton1 instance  = null;//将该实例用static进行声明
	public static Singleton1 Instance
	{
		get
		{	
			if(instance==null)//只有在instance ==null 的时候才会创建,确保创建一个
				instance = new Singleton1();
				
			return instance;
		}
	}
}

总结:注意这种方式在多线程中肯定是有问题的,两个线程有可能同时进行判断,然后同时进行创建,从而违背了单例模式的初衷。
不好的方法2:虽然在多线程环境中能工作但效率不高
code:

public class Singleton1
{
	private Singleton1()//将构造函数的访问权限级别设置为private,防止他们访问
	{
	}
	private static Singleton1 instance  = null;//将该实例用static进行声明
	public static Singleton1 Instance
	{
		get
		{	lock(synObj)
			{
				if(instance==null)//只有在instance ==null 的时候才会创建,确保创建一个
				instance = new Singleton1();
			}				
				
			return instance;
		}
	}
}

总结:这里加了一个锁,杜绝了多线程同时创建的情况。但我们每次通过instance属性得到整个单例的时候,都会加上一个锁,而实际上加锁是一种很耗时的操作的。

可行的解决方法

public sealed class Singleton3
{
	private Singleton3()
	{
	}
	private static object syncObj = new object();
	
	private static Singleton3 instance = null;
	public static Singleton3 Instance
	{
		get
		{
			if(instance ==null)
			{
				lock(syncObj)
				{
					if(instance==null)
						instance = new Singleton3();
					
				}
			}
		}
	}
}

总结:这个是在加锁之前做了一次instance是否为空的判断,从而避免每次在获取单例的时候,都会去加一次锁。

在单链表的末尾添加节点

void AddToTail(ListNode** pHead,int value)
{
	ListNode* pNew = new ListNode();
	pNew->m_nValue = value;
	pNew->m_pNext = Null;
	
	if(*pHead==NULL)//如果没有头结点,就将刚才新建的节点作为头结点
	{
		*pHead = pNew;
	}	
	else
	{
		ListNode* pNode = *pHead;//获取到头结点
		while(pNode->m_pNext!=NULL)//用一个while循环不断的往下寻找,直到找到最后面
			pNode = pNode->m_pNext;
		pNode->m_pNext = pNew;//将该节点跟在链表的最后一个节点的后面。
	}
}

总结:第一步肯定是先新建一个几点,然后接下来的步骤就是将该节点接到要接上的那个链表的上面,若链表为空,头指针肯定就是改节点,而如果不为空的话,就要不断的找啊找,找到最后面,然后接上去。

删除单链表中的某个节点

code:

void RemoveNode(ListNode** pHead,int value)
{
	if(pHead==NULL||*pHead==NULL)
		return;
	ListNode* pToBeDeleted = NULL;
	if((*pHead)->m_nValue==value)//若头结点就是要删除的值
	{
		pToBeDeleted= *pHead;//就将该节点确认下来,赋值给要删除的节点,从而在后面进行删除。
		*pHead = (*pHead)->m_pNext;//然后将头指针就该往后边进行移动了。

	}
	else
	{
		ListNode* pNode = *pHead;
		while(pNode->m_pNext!=NULL&&pNode->m_pNext->m_nValue!)//若不是那个头结点的话,就要不断的往后面进行移动了
			pNode = pNode->m_pNext;
		if(pNode->m_pNext!=NULL&&pNode->m_pNext->m_nValue==value)//接下来,再次执行一个判断,获知该节点就是要找的节点,然后,将指向该节点的指针往后拉
		{
			pToBeDeleted = pNode->m_pNext;
			pNode->m_pNext = pNode->m_pNext->m_pNext;

		}
		
	}
	if(pToBeDeleted!=NULL)//接下来就是对该节点进行删除了
	{
		delete pToBeDeleted;
		pToBeDeleted= NULL;
	}
}

快速排序

题目
实现快速排序算法的关键在于先在数组中选择一个数字,接下来把数 组中的数字分为两部分,比选择的数字小的数字移到数组的左边,比选择 的数字大的数字移到数组的右边。
code:先对数据进行划分

int Partition(int data[],int length,int start,int end)
{
	if(data==NULL||length<=0||start<0||end>=length)//对传入的肯定要进行判断,判断是否是没问题的
		throw new std::exception("Invalid Parameters");
	int index = RandomInRange(start,end);//考虑从start到end中挑出一个值
	Swap(&data[index],&data[end]);//将该数和最后一个数进行交换
	
	int small = start -1;
	for(index = start;index<end;++index)
	{
		if(data[index]<data[end])
		{
			++small;
			if(small!=index)
			{
				Swap(&data[index],&data[small]);
			}
		}
	}
	++small;
	Swap(&data[small],&data[end]);
	return small;
}

接下来就是进行递归排序:

void QuickSort(int data[],int length,int start,int end)//接下来就可以进行递归排序
{
	if(start==end)
		return;
	int index = Partition(data,length,start,end);//先选出一个数,将数组中比那个值要大的放在那个值的右边,小的放在左边
	if(index>start)//然后,就可以使用递归不断的去执行这个过程,就可以完成排序的过程
		QuickSort(data,length,start,index-1);
	if(index<end)
		QuickSort(data,length,index+1,end);
}

题目

面试题3:二维数组查找

题目:在这里插入图片描述
在这里插入图片描述

解题方法
在这里插入图片描述

面试题4:替换空格

题目
在这里插入图片描述
解法
在这里插入图片描述
总结:从后往前,先计算出总共需要多少个位置,然后,两个指针,解决所有问题。时间复杂度为0(N)。要注意内存覆盖的问题,能清楚的意识到字符替换之后,会出现比原来的字符串还要长的长度。

面试5:从尾到头打印链表

题目
在这里插入图片描述
方法一:栈循环
code

void PrintListReverSingly_Iteratively(ListNode* pHead)
{
	std::stack<ListNode*> nodes;//栈结构的声明
	ListNode* pNode = pHead;
	while(pNode!=NULL)//将节点压入栈中
	{
		nodes.push(pNode);
		pNode = pNode->m_pNext;
	}
	
	while(!nodes.empty())//当栈不为空的时候,不断的从栈中取出节点
	{
		pNode = nodes.top();
		printf("%d\t",pNode->m_pValue);
		nodes.pop();
	}
}

总结
关于该道题,肯定一想就是想到使用栈了,这个基本是毫无疑问的。
方法二:递归

void PrintListReversingly_Recursively(ListNode *pHead)
{
	if(pHead!=NULL)
	{
		if(pHead->m_pNext!=NULL)
		{
			PrintListReversingly_Recursively(pHead->m_pNext);
		}
		printf("%d",pHead->value);
	}
}

总结:但要注意,递归的太深,容易出现栈溢出的情况。

面试6:重建二叉树

题目
在这里插入图片描述
code
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

code

BinaryTreeNode* Construct(int* preorder,int* inorder,int length)
{
	if(preorder==NULL||inorder==NULL||length<=0)
		return NULL;
	return ConstructCore(preorder,preorder+length-1,inorder,inorder+length-1);//前两个是前序排列,后两个是中序排列
}

BinaryTreeNode* ConstructCore(int* startPreorder,int* endPreorder,int* startInorder,int *endInorder)
{
	int rootValue = startPreorder[0];//确定前序排列的第一个元素是根节点
	BinaryTreeNode* root = new BinaryTreeNode();//创建出那个要返回的树形结构
	root->m_nValue = rootValue;//将跟节点的值和左右节点先给解决了
	root->m_pLeft = root->m_pRight = NULL;
	if(startPreorder==endPreorder)//若只有一个节点
	{
		if(startInorder==endInorder&&*startPreorder ==*startInorder)//这真的满足只有一个节点的情况,就返回该根节点
			return root;
		else
			throw std::exception("Invalid input");
	}
	//在中序遍历中找到根节点的值
	int* rootInorder = startInorder;
	while(rootInorder<=endInorder&&*rootInorder!=rootValue)
		++rootInorder;
	if(rootInorder==endInorder&&*rootInorder!=rootValue)
		throw std::exception("Invalid input");
	
	int leftLength = rootInorder-startInorder;//在中序排列中在根节点左边的元素的个数
	int* leftPreorderEnd = startPreorder+leftLength;//找到排在前序排列中根节点左边的最右边元素
	if(leftLength>0)
	{
		//构建左子树
		root->m_pLeft = ConstructCore(startPreorder+1,leftPreorderEnd,startInorder,rootInorder-1);
	}
	if(leftLength<endPreorder-startPreorder)
	{
		//构建右子树
		root->m_pRight = ConstructCore(leftPreorderEnd+1,endPreorder,rootInorder+1,endInorder);
	}
}

总结:其实这个总结起来,就是通过传入前序排列的第一个节点和最后一个节点,和中序排列的第一个节点,和最后一个节点。递归,就可以求出要求出的那棵树。

面试题7:用两个栈实现一个队列

题目
在这里插入图片描述


template <typename T> class CQueue
{
	public:
		CQueue(void);
		~CQueue(void);
		
		void appendTail(const T& node);
		T deleteHead();
		
	private:
		stack<T> stack1;
		stack<T> stack2;
};

答案
在这里插入图片描述code

template<typename T> void CQueue<T>::appendTail(const  T& element)
{
	stack1.push(element);//在队列的末尾添加的元素的时候,就直接在第一栈的后面添加元素的就可以了。
}

template<typename T> T CQueue<T>::deleteHead()
{
	if(stack2.size()<=0)//如果第二个栈为空的话
	{
		while(stack1.size()>0)//但如果第一个栈不空的话。就要考虑将第一个栈中的内容给放到第二栈之中
		{
			T& data= stack1.top();//先拿到第一个栈的最顶上的数据
			stack1.pop();
			stack2.push(data);
		}
		if(stack2.size()==0)
			throw new exception("queue is empty");
		//接下来,就要在栈二进行删除操作了
		T head = stack2.top();//栈二进行弹出操作
		stack2.pop();
		return head;
	}
}
					

总结
关于这个两个栈成一个队列的这种情况,一般就是利用好栈的先进后出的特性。然后,进行恰当的弹出,push入。

面试题8:旋转数组的最小数字

题目
在这里插入图片描述
解答
在这里插入图片描述
在这里插入图片描述
code:

int Min(int* numbers,int length)
{
	if(numbers==NULL||length<=0)
		throw new std::exception("Invalid parameters");
	int index1 = 0;//一个指针指向第一个元素 
	int index2 = length-1;//第二个指针指向最后一个元素
	int indexMid = index1;//未开始移动指针之前,先将indexMid的值设为index1,为了防止当这个翻转数组本身就是一个有序数组的情况之下 
	while(numbers[index1]>=numbers[index2])
	{
		if(index2-index1==1)
		{
			indexMid = index2;//若两个指针相邻的话,就将index2设为indexMid 
			break;
		}
		indexMid = (index1+index2)/2;//否则就修改indexMid的值为(index1+index2)/2
		if(numbers[index1]==numbers[index2]&&numbers[indexMid]==numbers[index1])
			return MidInOrder(numbers,index1,index2); //若出现这种情况就得进行顺序查找了 
		if(numbers[indexMid]>=numbers[index1])//若当前的indexMid的值属于前一个有序数组 
			index1 = indexMid;
		else if(numbers[indexMid]<=numbers[index2])//若当前的indexMid属于后一个有序数组 
		{
			index2 = indexMid;
		}
		
	}
	 
	return numbers[indexMid];
}

int MinInOrder(int* numbers,int index1,int index2)
{
	int result = numbers[index1];
	for(int i = index1+1;i<index2;i++)
	{
		if(result>numbers[i])
			result = numbers[i]; 
	} 
	return result;
 } 

面试题9:斐波那契数列

题目
在这里插入图片描述
解法一
code

long long Fib(unsigned int n)
{
	if(n<=0)
		return 0;
	if(n==1)
		return 1;
	return Fib(n-1)+Fib(n-2);
}

解法二

long long Fib(unsigned int n)
{
	int result[2] = {0,1};
	if(n<2)
		return result[n];
	long long fib1 = 1;
	long long fib2 = 0;
	long long fibN = 0;
	for(unsigned int i = 2;i<=n;++i)
	{
		finN = fib1 +fib2;
		fib2 = fib1;
		fib1 = fibN;
	}
	return fibN;
}

总结:这阵的时间复杂度为O(N).应该可以满足要求。

面试题10:二进制中1 的个数

题目
在这里插入图片描述
解法一
第一种是我自己的想法,就是将这个二进制数字转换为字符串,做一个循环,就可以获得这个字符串中1的个数,从而得到我们要的数字。但这个想法我是没有去实现的,可能实现的可能性也不大,可能有问题。
解法二
不断的左移输入的数字i,让其与1进行与操作,这样就可以判断输入的数字中有多少个1了。但存在的问题是,若输入的数字为负数的话,就可能出现问题。
解法三
不断的左移1,然后再和输入的数字进行与操作。这样就可以获得1的个数。并且不会出现上面的那种问题。

int NumberOf1(int n )
{
	int count = 0;
	unsigned int flag = 1;
	while(flag)
	{
		if(n&flag)
			count++;
		flag = flag<<1;	
	}
	return count;	
} 

参考

  1. 剑指offer 名企面试官精讲典型编程题.pdf
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值