一,复杂链表的复制(链表、算法)
1)题目:有一个复杂链表,其结点除了有一个m_pNext指针指向下一个结点外,还有一个m_pSibling指向链表中的任一结点或者NULL。其结点的C++定义如下:参考【转载】
struct ComplexNode
{
int m_nValue;
ComplexNode* m_pNext;
ComplexNode* m_pSibling; //指向任意一个结点
};
图是一个含有5个结点的该类型复杂链表。图中实线箭头表示m_pNext指针,虚线箭头表示m_pSibling指针。为简单起见,指向NULL的指针没有画出。
方法一:看到这个问题,我的第一反应是分成两步:
第一步:是复制原始链表上的每个链表,并用m_pNext链接起来;
第二步,假设原始链表中的某节点N的m_pSibling指向结点S,由于S的位置在链表上有可能在N的前面也可能在N的后面,所以要定位N的位置需要从原始链表的头结点开始找,假设从原始链表的头结点开始经过s步找到结点S,那么在复制链表上结点N的m_pSibling的S’,离复制链表的头结点的距离也是s,用这种办法我们就能为复制链表上的每个结点设置m_pSibling了。对含有n个结点的链表,由于定位每个结点的m_pSibling都需要从链表头结点开始经过O(n)步才能找到,因此这种方法的总时间复杂度是O(n2)。
方法二:空间换时间:由于上述方法的时间主要花费在定位结点的m_pSibling上面,我们试着在这方面去做优化,还是分为两步
第一步仍然是复制原始链表上的每个结点N,并创建N’,然后把这些创建出来的结点链接起来,这里我们对<N,N’>的配对信息放到一个哈希表中;
第二步还是设置复制链表上每个结点的m_pSibling,如果在原始链表中结点N的m_pSibling指向结点S,那么在复制链表中对应的N’应该指向S’,由于有了哈希表,可以用O(1)的时间根据S找到S’。,相当于用空间换时间,以O(n)的空间消耗实现了O(n)的时间效率
方法三:换一种思路,在不用辅助空间的情况下实现O(n)的时间效率。在原来基础上创建节点,然后拆除。
第一步:根据原始链表的每个结点N,创建对应的N’。这一次,我们把N’链接在N的后面。
【关键】注意是在原来每个节点后面,创建一个该节点的拷贝
// Clone all nodes in a complex linked list with head pHead, and connect all nodes with m_pNext link void CloneNodes(ComplexNode* pHead) { ComplexNode* pNode = pHead; while(pNode != NULL) { ComplexNode* pCloned = new ComplexNode(); pCloned->m_nValue = pNode->m_nValue; pCloned->m_pNext = pNode->m_pNext; pCloned->m_pSibling = NULL; pNode->m_pNext = pCloned; pNode = pCloned->m_pNext; } }
// Connect sibling nodes in a complex link list void ConnectSiblingNodes(ComplexNode* pHead) { ComplexNode* pNode = pHead; while(pNode != NULL) { ComplexNode* pCloned = pNode->m_pNext; if(pNode->m_pSibling != NULL) pCloned->m_pSibling = pNode->m_pSibling->m_pNext; pNode = pCloned->m_pNext; } }
// Split a complex list into two: Reconnect nodes to get the original list, and its cloned list ComplexNode* ReconnectNodes(ComplexNode* pHead) { ComplexNode* pNode = pHead; ComplexNode* pClonedHead = NULL; ComplexNode* pClonedNode = NULL; if(pNode != NULL) { pClonedHead = pClonedNode = pNode->m_pNext; pNode->m_pNext = pClonedNode->m_pNext; pNode = pNode->m_pNext; } while(pNode != NULL) { pClonedNode->m_pNext = pNode->m_pNext; pClonedNode = pClonedNode->m_pNext; pNode->m_pNext = pClonedNode->m_pNext; pNode = pNode->m_pNext; } return pClonedHead; }
// Clone a complex linked list with head pHead ComplexNode* Clone(ComplexNode* pHead) { CloneNodes(pHead); ConnectSiblingNodes(pHead); return ReconnectNodes(pHead); }
二,链表的面试题目如下(链表):
【1】给定单链表,检测是否有环
使用两个指针p1,p2从链表头开始遍历,p1每次前进一步,p2每次前进两步。如果p2到达链表尾部,说明无环,否则p1、p2必然会在某个时刻相遇(p1==p2),从而检测到链表中有环。
【2】给定两个单链表(head1,head2),检测两个链表是否有交点,如果有返回第一个交点。
1)如果head1==head2,那么显然相交,直接返回head1。
2)否则,分别从head1,head2开始遍历两个链表获得其长度len1与len2,假设len1>=len2,那么指针p1由head1开始向后移动len1-len2步,指针p2=head2,
3)下面p1、p2每次向后前进一步并比较p1p2是否相等,如果相等即返回该结点,否则说明两个链表没有交点。
【3】给定单链表(head),如果有环的话请返回从头结点进入环的第一个节点。【判断有环,分成两个单链表,求交点】
运用题一,我们可以检查链表中是否有环。
如果有环,那么p1p2重合点p必然在环中。从p点断开环,
方法为:p1=p,p2=p->next,p->next=NULL。此时,原单链表可以看作两条单链表,一条从head开始,另一条从p2开始,于是运用题二的方法,我们找到它们的第一个交点即为所求。
【4】只给定单链表中某个结点p(并非最后一个结点,即p->next !=NULL) 指针,删除该结点【狸猫换太子】
办法很简单,首先是放p中数据,然后将p->next的数据copy入p中,接下来删除p->next即可。
【5】只给定单链表中某个结点p(非空结点),在p前面插入一个结点。【狸猫换太子】
办法与前者类似,首先分配一个结点q,将q插入在p后,接下来将p中的数据copy入q中,然后再将要插入的数据记录在p中。
三,链表和数组的区别在哪里(链表、数组)
链表:元素在内存中非连续存放,用next指向下一个元素。
查找任意元素慢
添加、删除元素快,不需要移动其它的元素
动态分配内存的形式实现。需要时可以用new分配内存空间,不需要时用delete将已分配的空间释放,不会造成内存空间的浪费
数组:元素在内存中连续存放,通过下标迅速访问数组中任何元素。
四,(链表、字符串)
1,编写实现链表排序的一种算法。说明为什么你会选择用这样的方法?
#include <iostream> using namespace std; struct node { int data; node *next; }; void swap(node *x,node *y) { int temp=x->data; x->data=y->data; y->data=temp; } node* createLink(int a[],int n) { node *head=new node(); node *temp=head; for(int i=0;i<n;i++) { node *p=new node(); p->data=a[i]; temp->next=p; temp=p; } return head; } node *sort(node *head)//选择排序 { node *p=head->next; node *q=head->next; node *temp=p; while(p) { temp=p; while(q) { if(temp->data > q->data) { temp= q; } q=q->next; } swap(temp,p); p=p->next; q=p; } return head; } void print(node *head) { node *p=head->next; while(p) { cout<<p->data<<" "; p=p->next; } } int main() { int a[]={1,2,6,4,3,5}; node *head=createLink(a,6); print(head); cout<<endl; node *sortHead= sort(head); print(sortHead); }
2,编写实现数组排序的一种算法。说明为什么你会选择用这样的方法?
#include "stdio.h" void HeapAdjust(int array[],int i,int nLength)//调整堆 { int nChild; int nTemp;//赋值为待调整的 节点 //从应该调整的节点开始 ,直到该节点 已经没有孩子节点 for(nTemp=array[i];2*i+1<nLength;i=nChild)//由于根节点从0开始计数 故左孩子 为2*i+1 { nChild=2*i+1;//子节点=2(父节点)+1 /*一共两个子节点的话得到 较大的一个*/ //nChild<nLength-1 判断到头没有 if(nChild<nLength-1&&array[nChild+1]>array[nChild]) ++nChild; /*如果较大子节点大于父节点 将子节点 调整到父节点*/ if(nTemp<array[nChild]) array[i]=array[nChild]; else break;//这个地方不加 会出错 第一个会输出第二个 array[nChild]=nTemp;//子节点 等于父节点 } } void HeapSort(int a[],int length) { /*初建堆 */ for(int i=length/2-1;i>=0;--i)//从最后一个 非叶子节点调整 (这里的 i是下标) HeapAdjust(a,i,length); int temp; for(int i=length-1;i>0;--i) { /*第一个最大元素跟最后一个交换*/ temp=a[0]; a[0]=a[i]; a[i]=temp; HeapAdjust(a,0,i);//调整堆 (注意 length=i 由于堆是逐渐变小的) } } int main() { int a[10]={1,2,5,3,8,4,7,6}; HeapSort(a,8); printf("对数组1,2,5,3,8,4,7,6 进行从小到大的堆排序\n"); for(int i=0;i<8;i++) printf("%d ",a[i]); return 0; }
3,请编写能直接实现strstr()函数功能的代码。
#include <iostream> using namespace std; /*功能:找出str2字符串在str1字符串中第一次出现的位置(不包括str2的串结束符)。 返回值:返回该位置的指针,如找不到,返回空指针。*/ char *MyStrstr( char *str1, char *str2) { int i=0,j=0; if(strlen(str1) <strlen(str2)) return NULL; while(*(str1+i) && *(str2+j)) { if(*(str1+i)==*(str2+j)) { i++; j++; } else { i=i-j+1; j=0; } } return (j> 0)?str1+i-j:NULL; } int main() { char *str1= "My name is tianshuai"; char *str2= "is"; cout<< strstr(str1,str2)<<endl; cout<< MyStrstr(str1,str2)<<endl; }
5,编写反转字符串的程序,要求优化速度、优化空间。
方法一:空间复杂度 O(n) 时间复杂度 O(n)
#include <iostream> using namespace std; char* strrev1(const char* str) { int len=strlen(str); char *str2=new char[len+1]; //需要申请内存地址 strcpy(str2,str);//将只读字串复制到刚申请的地址中 //char str2[len];//这种方法不成 //memset(str2, '0' , sizeof(char)*len); for(int i=0;i<len;++i) str2[i]=str[len-i-1]; //str2[len]='\0'; return str2; } int main() { char *str="hello"; cout<<strrev1(str)<<endl; getchar(); }
交换两个数的值
int a=2; int b=3; a^=b; b^=a; a^=b; cout<<a<<endl; cout<<b<<endl;
方法二:递归
方法三:如果参数不是const 则时间复杂度 O(n) 空间复杂度 不需要(采用上述交换策略)
6,在链表里如何发现循环链接?
使用两个指针p1,p2从链表头开始遍历,p1每次前进一步,p2每次前进两步。如果p2到达链表尾部,说明无环,否则p1、p2必然会在某个时刻相遇(p1==p2),从而检测到链表中有环。
7.给出洗牌的一个算法,并将洗好的牌存储在一个整形数组里
实际上是打乱一个数组
首先54张牌分别用0到53 的数值表示并存储在一个整形数组里,数组下标代表纸牌所在的位置。接下来,遍历整个数组,在遍历过程中随机产生一个随机数,并以该随机数为下标的数组元素与当前遍历到的数组元素进行对换。时间复杂度为O(n) (注:所得到的每一种结果的概率的分母越大越好)
#include <stdio.h> #include <time.h> #include <stdlib.h> void shuffle(int boke[]) //洗牌 { int i,r,t; srand((unsigned)time(NULL)); //随机数种子 for(i=0; i<54; i++) { r=(rand()%107)/2; //交换 t=boke[i]; boke[i]=boke[r]; boke[r]=t; } } int main() { int boke[54],i; for(i=0;i<54;i++) //初始化纸牌 boke[i]=i; printf("before shuffle:\n"); for(i=0; i<54; i++) //打印 printf("%d ",boke[i]); shuffle(boke); //洗牌 printf("\nafter shuffle:\n"); for(i=0; i<54; i++) //打印 printf("%d ",boke[i]); return 0; }
8.写一个函数,检查字符是否是整数,如果是,返回其整数值。(或者:怎样只用4行代码编写出一个从字符串到长整形的函数?)
if(ch>'0' && ch <'9')
return ch-'0';
else
return false;
#include <stdlib.h> #include <stdio.h> #include <string> #include <iostream> using namespace std; long strtoint(char *str,int length); long strtoint(char *str,int length) { if(length > 1) return str[0]=='-' ? strtoint(str, length-1)*10-(str[length-1]-'0') : strtoint(str, length-1)*10+str[length-1]-'0'; else return str[0]=='-' ? -1/10 : str[0]-'0'; } int main(int argc, char* argv[]) { long n=strtoint("123",3); cout<<n<<endl; cout<<strtoint("-123",4)<<endl; return 0; }
9,给出一个函数来输出一个字符串的所有排列。
10.请编写实现malloc()内存分配函数功能一样的代码。
void * malloc(unsigned int size); //需要时申请内存
void free(void *ptr); //不需要时释放内存
---->参考博客
malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时
1)它沿连接表寻找一个大到足以满足用户请求所需要的内存块。
2)然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。
3)接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。
调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。
malloc()在操作系统中的实现
在 C 程序中,多次使用malloc () 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。
在大部分操作系统中,内存分配由以下两个简单的函数来处理:
void *malloc (long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。
malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量:
//清单 1. 我们的简单分配程序的全局变量
int has_initialized= 0;
void *managed_memory_start;
void *last_valid_address;
如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX? 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:
清单 2. 分配程序初始化函数
/* Include the sbrk function*/
#include
void malloc_init()
{
/* grab the last valid address from the OS*/
last_valid_address = sbrk(0);
/* we don''t have any memory to manage yet, so
*just set the beginning to be last_valid_address
*/
managed_memory_start = last_valid_address;
/* Okay, we''re initialized and ready to go*/
has_initialized =1;
}
现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:
//清单 3. 内存控制块结构定义
struct mem_control_block {
int is_available;
int size;
};
现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。
在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:
清单 4. 解除分配函数
void free(void*firstbyte) {
struct mem_control_block*mcb;
/* Backup from the given pointer to find the
* mem_control_block
*/
mcb = firstbyte- sizeof(struct mem_control_block);
/* Mark the block as being available*/
mcb->is_available= 1;
/* That''s It! We''re done.*/
return;
}
如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码:
//清单 6. 主分配程序
void*malloc(long numbytes) {
/* Holds where we are looking in memory*/
void*current_location;
/* This is the same as current_location, but cast to a
* memory_control_block
*/
struct mem_control_block*current_location_mcb;
/* This is the memory location we will return. It will
* be set to 0 until we find something suitable
*/
void*memory_location;
/* Initialize if we haven''t already done so*/
if(! has_initialized) {
malloc_init();
}
/* The memory we search for has to include the memory
* control block, but the users of malloc don''t need
* to know this, so we''ll just add it in for them.
*/
numbytes = numbytes+ sizeof(struct mem_control_block);
/* Set memory_location to 0 until we find a suitable
* location
*/
memory_location =0;
/* Begin searching at the start of managed memory*/
current_location = managed_memory_start;
/* Keep going until we have searched all allocated space*/
while(current_location!= last_valid_address)
{
/* current_location and current_location_mcb point
* to the same address. However, current_location_mcb
* is of the correct type, so we can use it as a struct.
* current_location is a void pointer so we can use it
* to calculate addresses.
*/
current_location_mcb =
(struct mem_control_block*)current_location;
if(current_location_mcb->is_available)
{
if(current_location_mcb->size>= numbytes)
{
/* Woohoo! We''ve found an open,
* appropriately-size location.
*/
/* It is no longer available*/
current_location_mcb->is_available= 0;
/* We own it*/
memory_location = current_location;
/* Leave the loop*/
break;
}
}
/* If we made it here, it''s because the Current memory
* block not suitable; move to the next one
*/
current_location = current_location+
current_location_mcb->size;
}
/* If we still don''t have a valid location, we''ll
* have to ask the operating system for more memory
*/
if(! memory_location)
{
/* Move the program break numbytes further*/
sbrk(numbytes);
/* The new memory will be where the last valid
* address left off
*/
memory_location = last_valid_address;
/* We''ll move the last valid address forward
* numbytes
*/
last_valid_address = last_valid_address+ numbytes;
/* We need to initialize the mem_control_block*/
current_location_mcb = memory_location;
current_location_mcb->is_available= 0;
current_location_mcb->size= numbytes;
}
/* Now, no matter what (well, except for error conditions),
* memory_location has the address of the memory, including
* the mem_control_block
*/
/* Move the pointer past the mem_control_block*/
memory_location = memory_location+ sizeof(struct mem_control_block);
/* Return the pointer*/
return memory_location;
}
这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可.多次调用malloc()后空闲内存被切成很多的小内存片段,这就使得用户在申请内存使用时,由于找不到足够大的内存空间,malloc()需要进行内存整理,使得函数的性能越来越低。聪明的程序员通过总是分配大小为2的幂的内存块,而最大限度地降低潜在的malloc性能丧失。也就是说,所分配的内存块大小为4字节、8字节、16字节、18446744073709551616字节,等等。这样做最大限度地减少了进入空闲链的怪异片段(各种尺寸的小片段都有)的数量。尽管看起来这好像浪费了空间,但也容易看出浪费的空间永远不会超过50%。
11.给出一个函数来复制两个字符串A和B。字符串A的后几个字节和字符串B的前几个字节重叠。
例如:给定 str1="abcd"; str2="cdcsd";
则 res="abcdcsd";
思路:找出str1末尾与str2重叠的部分
源码:
#include <iostream> #include <string> using namespace std; int main() { char *str1="abcdefgh"; char *str2="defghijkl"; int pos=0; for(int i=0;i<strlen(str1);++i) { if(strstr(str2,str1+i) == str2)//str1+i 为前缀 { pos=i; break; } } int size=strlen(str1)+strlen(str2)-pos+1; char *str=(char*)malloc(size*sizeof(char)); strcpy(str,str1); strcat(str,str2+ (strlen(str2)-1)-pos); cout<<str<<endl; }
12.怎样编写一个程序,把一个有序整数数组放到二叉树中?
题意:建立一个中序遍历是有序的二叉树
解答:采用递归,每次创建的节点都是 数组中间的元素 mid =length>>1 而当数组长度==0 时候,则返回NULL叶子
#include "stdio.h" #include "stdlib.h" #include "string.h" struct student { int value; struct student *lchild; struct student *rchild; }; void arraytotree(int *a, int len, struct student **p) { if(len) { *p = (struct student*)malloc(sizeof(struct student)); (*p)->value = a[len/2]; arraytotree(a, len/2, &((*p)->lchild)); arraytotree(a+len/2+1, len-len/2-1, &((*p)->rchild)); } else { *p = NULL; } } void display_tree(struct student *head) { if(head->lchild)display_tree(head->lchild); printf("%d\t", head->value); if(head->rchild)display_tree(head->rchild); } int main() { int a[] = {1,2,3,4,9,10,33,56,78,90}; struct student *tree; arraytotree(a, sizeof(a)/sizeof(a[0]), &tree); printf("After convert:\n"); display_tree(tree); printf("\n"); return 0; }
13.怎样从顶部开始逐层打印二叉树结点数据?请编程。
#include <cstdio> #include <cstdlib> #include <string> #include <iostream> #include <deque> using namespace std; struct student { int value; student *lchild; student *rchild; }; void arraytotree(int *a, int len, student **p) { if(len) { *p = (student*)malloc(sizeof(student)); (*p)->value = a[len/2]; arraytotree(a, len/2, &((*p)->lchild)); arraytotree(a+len/2+1, len-len/2-1, &((*p)->rchild)); } else { *p = NULL; } } void display_tree(student *head) { if(head->lchild)display_tree(head->lchild); printf("%d\t", head->value); if(head->rchild)display_tree(head->rchild); } void Level_display(student *head) { deque<student*> queue; queue.push_back(head); while(queue.size()) { cout<<queue.front()->value<<" "; if(queue.front()->lchild!=NULL) queue.push_back(queue.front()->lchild); if(queue.front()->rchild!=NULL) queue.push_back(queue.front()->rchild); queue.pop_front(); } } int main() { int a[] = {1,2,3,4,5,6,7}; student *tree; arraytotree(a, sizeof(a)/sizeof(a[0]), &tree); printf("After convert:\n"); display_tree(tree); printf("\n"); printf("Level Display:\n"); Level_display(tree); printf("\n"); return 0; }
14.怎样把一个链表掉个顺序(也就是反序,注意链表的边界条件并考虑空链表)?
Lnode* Reverse(Lnode *head) { Lnode * p; Lnode * q; p=head-> next; head-> next=NULL; while(p!=NULL) { q = p-> next; p-> next = head; head =p; p = q; } return head; }
五,阿里巴巴一道笔试题(运算、算法)
问题描述:
12个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高,问排列方式有多少种?
这道题其实考察的是,卡特兰数。类似的题目为,10个小朋友去买门票,5个拿着1元,5个拿着2元。而售货员自己没有零钱,问所有人可以买到票的排序方式有多少?
卡特兰公式为:C(2n,n)/(n+1) 本题 n=6
参考博文 :http://blog.csdn.net/tianshuai11/article/details/7838142
http://blog.csdn.net/tianshuai11/article/details/7557619