《剑指offer》面试编程题之数据结构示例

一、数组

  • 内存连续 ——> 时间效率高 ——> 实现简单哈希表
  • 空间效率不高 ——> 实现多种动态数组 ——> 使用时要尽量减少改变数据容量大小的次数
  • 了解其与指针的区别
    int GetSize(int data[]){
    	return sizeof(data);
    }
    int main(){
    	int data1[]={1,2,3,4,5};
    	int size1=sizeof(data1); //求数组大小,每个整数4个字节,4*5=20
    	
    	int *data2=data1;
    	int size2=sizeof(data2); //本质是指针,32位系统4,64位系统8
    	
    	int size3=GetSize(data1);  
        //当数组作为函数参数进行传递时,数组会自动退化为同类型的指针 故输出等同size
    	
    //	cout<<size1<<" "<<size2<<" "<<size3<<endl;
    	return 0;
    }

面试题3 数组中重复数字 

题目一 找出数组中的重复数字

在一个长度为n的数组里的所有数字都在0~n-1的范围内

题目要求数组中的数字都在0~n-1的范围内。

——> 如果这个数组中没有重复数字,那么当数组排序后数字i将出现在下标为i的位置,

——> 若有,则排序后有的位置存在多个数字或者可能没有数字。

{2,3,1,0,2,5,3}
{1,3,2,0,2,5,3}
{3,1,2,0,2,5,3}
{0,1,2,3,2,5,3}

bool duplicate(int numbers[],int length,int *duplication){
	if(numbers==nullptr||length<=0) return false;
	for(int i=0;i<length;i++){
		if(numbers[i]<0||numbers[i]>length-1) return false;
	}
	for(int i=0;i<length;i++){
		while(numbers[i]!=i){  //如果数字m不等于下标i,
			if(numbers[i]==numbers[numbers[i]]) { 
				//如果该数字m与该数字作为下标m的数字numbers[m]相等,重复出现!
				*duplication=numbers[i];
				return true;
			}
			//如果不相等,就交换
//			int temp=numbers[i]; 
//			numbers[i]=numbers[temp];
//			numbers[temp]=temp;
			swap(numbers[i],numbers[numbers[i]]);
		}
	}
	return false;
}
int main(){
	int d[]={2,3,1,0,2,5,3};
	int length=sizeof (d) / sizeof (d[0]);
	int *res;
	cout<<duplicate(d,length,res)<<endl;
	cout<<*res<<endl;
	return 0;
}
题目二 不修改数组找出重复的数字 

在一个长度为n+1的数组里的所有数字都在1~n的范围内

假如没有重复数字,那么1~n范围内在数组中出现的次数为n

——> 如果1~n范围内的数字出现次数超过n,则一定包含了重复数字

按照二分查找的思路

{2,3,5,4,3,2,6,7} 1-7
{1,2,3,4}->5 {5,6,7}
{1,2}->2 {3,4}->3 //需要指出,该算法不能保证找出所有重复的数字,不能确定每个数字各出现一次还是某个数字出现了多次
{3}->2 {4}

//统计start~end范围内的数字出现总次数
int countRange(const int *numbers,int length,int start,int end){
	if(numbers==nullptr) return 0;
	int count=0;
	for(int i=0;i<length;i++){
		if(numbers[i]>=start&&numbers[i]<=end) count++;
	}
	return count;
}
int getDuplication(const int *numbers,int length){
	if(numbers==nullptr||length<0) return -1;
	int start=1;
	int end=length-1;
	while(end>=start){
		int middle=((end-start)>>1)+start;
		int count=countRange(numbers,length,start,middle);
		if(end==start){
			if(count>1) return start;
			else break;
		}
		if(count>(middle-start+1)) end=middle;
		else start=middle+1;
	}
	return -1;
}
int main(){
	int d[]={2,3,5,4,3,2,6,7};
	int length=sizeof (d) / sizeof (d[0]);
	cout<<getDuplication(d,length)<<endl;
	return 0;
}

面试题4 二维数组中的查找

每一行都从左到右递增,每一列都从上到下递增

首先选取数组中右上角的数字

——> 如果该数字等于要查找的数字,则查找过程结束,

——> 如果该数字大于要查找的数字,则剔除这个数字所在列,

——> 如果该数字小于要查找的数字,(可能出现在该数字右边和下边,右边的列已经被剔除),则剔除这个数字所在行,

这样每一步都可以缩小查找范围,直到找到要查找的数字,或者查找范围为空

1 2 8 9

2 4 9 12 

4 7 10 13

6 8 11 15

—> 查找7—> 9>7

1 2 8 

2 4 9

4 7 10

6 8 11

——> 8>7

1 2

2 4

4 7

6 8

——> 2<7

2 4

4 7

6 8

——> 4<7

4 7

6 8

bool Find(int *matrix,int rows,int columns,int number){
	if(matrix!=nullptr&&rows>0&&columns>0){
		int row=0;
		int column=columns-1;
		while(row<rows&&column>=0){
			int temp=matrix[row*columns+column];
			if(temp==number) return true; //找到
			else if(temp>number) --column; //大于
			else ++row; //小于
		}
	}
	return false;
}

 二、字符串 

  •  每个字符串以‘\0’作为结尾 ——> 注意实际长度加1
  • 为节省内存,把常量字符串放到单独的一个内存区域,当几个指针赋值给相同的常量字符串时,它们实际上会指向相同的内存地址。但用常量内存初始化数组,情况不同。
    int main(){
    	char str1[]="hello";
    	char str2[]="hello"; //str1!=str2
        // 两个字符串数组 先分配空间,再把内容复制到数组中去
    	
    	char *str3="hello";
    	char *str4="hello"; //str3==str4
        //两个指针 只需要指向“hello”在内存中的地址 
        //“hello”是常量字符串,在内存中只有一个拷贝,因此指向同一个地址
    
    	return 0;
    }

 面试题5 替换空格

 在网络编程中,如果url参数中含有特殊字符,如空格、‘#'等,则可能导致服务器端无法获得正确的参数值。

——> 我们就需要把这些特殊符号转换成服务器可以识别的字符。

——> 转换规则是’%‘后面跟上ASCII码的两位十六进制表示。空格 —> “%20” 、’#‘—> “%23”

首先想到字符串会变长

1. 如果是在原来的字符串上进行替换,就有可能覆盖修改在该字符串后面的内存

2. 如果是创建新的字符串并在新的字符串上进行替换,那么我们可以自己分配足够多的内存

——> 向面试官问清楚,明确告诉我们需求 

如果是需求1,并且保证输入的字符串后面有足够多的内存 

1. 从前向后替换,每次碰到空格字符的时候进行替换,每次都必须要把空格后面所有的字符都后移2字节,否则就有两个字符被覆盖了。这样的话数组中很多字符都移动了很多次,如何减少移动次数?

2. 从后向前替换,减少移动次数,提高效率

——> 先遍历一次字符串,统计出字符串中空格的总数,由此计算出替换之后的字符串的总长度newL=原来的长度l+2*空格数目numberOfBlank

——> 准备两个指针p1和p2,p1指向原始字符串末尾,p2指向替换后的字符串末尾

——> 向前移动指针p1,逐个把它指向的字符复制到p2指向的位置,直到p1碰到第一个空格为止

——>p1碰到第一个空格后,在p2之前插入字符串“%20”(同时p2向前移动3格),然后p1向前移动一格

——> 重复移动步骤,直到p1和p2指向同一位置

这样的话,所有字符都只复制(移动)一次 

//length 为字符数组string的总容量
void ReplaceBlack(char str1[],int length){
	if(str1==nullptr||length<=0) return;
	int l=0; //字符串实际长度
	int numberOfBlank=0;
	int i =0;
	while(str1[i]!='\0'){
		++l;
		if(str1[i]==' ') ++numberOfBlank;
		++i;
	}
	int newL=l+2*numberOfBlank; //替换后的字符串长度
	if(newL>length) return;
	
	//两个指针
	int p1=l;
	int p2=newL;
	while(p1>=0&&p2>p1){
		if(str1[p1]==' '){
			str1[p2--]='0';
			str1[p2--]='2';
			str1[p2--]='%';
		}else{
			str1[p2--]=str1[p1];
		}
		--p1;
	}
}
int main(){
	char str1[]="We are happy.";
	ReplaceBlack(str1,19);
	cout<<str1<<endl;
	return 0;
}
举一反三 合并两个数组

两个排序的数组A1和A2,内存在A1的末尾有足够多的空余空间容纳A2。把A2中所有数字插入A1中,并且所有的数字是排序的。

1. 在A1中从头到尾复制数字,但这样就会出现多次复制一个数字的情况。

2. 从尾到头

——> 确定newL

——> 从尾到头比较数字,将较大的数字复制到A1中合适位置

A1 [2,5,7,9]

A2 [1,3,6,8]

[2,5,7,9,x,x,x,x]

[2,5,7,9,x,x,x,9]

[2,5,7,9,x,x,8,9]

[2,5,7,9,x,7,8,9]

[2,5,7,9,6,7,8,9]

[2,5,7,5,6,7,8,9]

[2,5,3,5,6,7,8,9]

[2,2,3,5,6,7,8,9]

[1,2,3,5,6,7,8,9]

void CollectArr(vector<int> a1,vector<int> a2){
	int a1l=a1.size(); //实际长度
	int a2l=a2.size();
	int newL=a1.size()+a2.size(); // 合并后的长度
	//两个指针
	int pa1=a1l-1;
	int pa2=a2l-1;
	int p2=newL-1;
	cout<<p2<<endl;
	while(p2>=0&&pa1>=0&&pa2>=0){
		int tempa1=a1[pa1];
		int tempa2=a2[pa2];
		cout<<tempa1<<" "<<tempa2<<endl;
		if(tempa1>=tempa2){
			a1[p2--]=tempa1;
			--pa1;
		}else{
			a1[p2--]=tempa2;
			--pa2;
		}
	}
	//a1或a2中剩余部分
	while(pa1>=0) a1[p2--]=a1[pa1--];
	while(pa2>=0) a1[p2--]=a2[pa2--];
	for(int i=0;i<newL;i++) cout<<a1[i]<<" ";
}
int main(){
	int a[]={2,5,7,9};
	int aa[]={1,3,6,8};
	vector<int> a1(a,a+4);
	vector<int> a2(aa,aa+4);
	CollectArr(a1,a2);
	return 0;
}

三、链表

  • 是一种动态数据结构
  • 没有限制的内存,链表的空间效率比数组高

往单向链表末尾添加一个节点 (尾插法)

struct ListNode{ //单向链表的节点定义
	int m_nValue;
	ListNode* m_pNext;
};
void AddToTail(ListNode** pHead,int value){ //第一个参数pHead是一个指向指针的指针
	ListNode* pNew = new ListNode(); //为新节点分配内存
	pNew->m_nValue=value;
	pNew->m_pNext=nullptr;
	
	if(*pHead==nullptr){ //往一个空链表中插入一个节点,新插入的节点就是链表的头指针
		// 这时会改动头指针,
		// 因此必须把pHead参数设为指向指针的指针,
		// 否则出来这个函数pHead仍然是一个空指针
		*pHead=pNew;
	}else{
		ListNode* pNode=*pHead;
		//无法保证链表的内存和数组一样是连续的,
		//因此只能沿着指向下一个节点的指针到达尾节点
		while(pNode->m_pNext!=nullptr) pNode=pNode->m_pNext; 
		pNode->m_pNext=pNew;
	}
}

在链表中找到第一个含有某值的节点并删除该节点代码

struct ListNode{ //单向链表的节点定义
	int m_nValue;
	ListNode* m_pNext;
};
void RemoveNode(ListNode** pHead,int value){ //第一个参数pHead是一个指向指针的指针
	if(pHead==nullptr||*pHead==nullptr) return;
	ListNode* pToBeDeleted=nullptr;
	if((*pHead)->m_nValue==value){ //如果该节点是头节点
		pToBeDeleted=*pHead;
		*pHead = (*pHead)->m_pNext;
	}
	else{
		ListNode* pNode=*pHead;
		//无法保证链表的内存和数组一样是连续的,
		//因此只能沿着指向下一个节点的指针到达要删除节点
		while(pNode->m_pNext!=nullptr && pNode->m_pNext->m_nValue != value) pNode=pNode->m_pNext; 
		
		if(pNode->m_pNext!=nullptr && pNode->m_pNext->m_nValue == value){ //找到
			pToBeDeleted=pNode->m_pNext;
			//删除该节点
			pNode->m_pNext=pNode->m_pNext->m_pNext;
		}
	}
	
	if(pToBeDeleted!=nullptr){ //处理删除节点
		delete pToBeDeleted;
		pToBeDeleted=nullptr;
	}
}

面试题6 从尾到头打印链表

 1. 从头到尾输出将会比较简单,可不可以将链表中链表节点的指针反转归来,改变链表的方向然后就可以从尾到头输出了。但这方法会改变原来链表的结构,是否允许在打印链表的时候修改链表的结构?

——> 面试中,如果我们打算修改输入的数据,最好先问面试官是否允许修改

2. 假如要求这道题目不能改变链表的结构

——> 先遍历一遍链表,典型的“后进先出” ——> 栈实现

法一 栈 
struct ListNode{ //单向链表的节点定义
	int m_nValue;
	ListNode* m_pNext;
};
void PrintListReversingly_Iteratively(ListNode* pHead){ //第一个参数pHead是一个指向指针的指针
	stack<ListNode*> nodes;
	ListNode* pNode=pHead;
	while(pNode!=nullptr){
		nodes.push(pNode);
		pNode=pNode->m_pNext;
	}
	while(!nodes.empty()){
		pNode=nodes.top();
		cout<<pNode->m_nValue<<" ";
		nodes.pop();
	}
}
 法二 递归

递归在本质上就是一种栈结构

——> 我们每访问到一个结点,先递归输出它后面的节点,再输出该节点自身

struct ListNode{ //单向链表的节点定义
	int m_nValue;
	ListNode* m_pNext;
};
void PrintListReversingly_Iteratively(ListNode* pHead){ //第一个参数pHead是一个指向指针的指针
	if(pHead!=nullptr){
		if(pHead->m_pNext!=nullptr){
			 PrintListReversingly_Iteratively(pHead->m_pNext);
		}
		cout<<pHead->m_nValue<<" ";
	}
}

 ——> 当链表非常长的时候,会导致函数调用的层级很深,从而有可能导致函数调用栈溢出

——> 显然用栈的代码鲁棒性要好一些


四、树 p60-67


五、栈和队列 p68-71

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

prominent.949

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值