一、数组
- 内存连续 ——> 时间效率高 ——> 实现简单哈希表
- 空间效率不高 ——> 实现多种动态数组 ——> 使用时要尽量减少改变数据容量大小的次数
- 了解其与指针的区别
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<<" ";
}
}
——> 当链表非常长的时候,会导致函数调用的层级很深,从而有可能导致函数调用栈溢出
——> 显然用栈的代码鲁棒性要好一些