面试常见问题之二(算法)
[基础算法]面试简单算法实现
https://github.com/iyjhabc/simple_algorithm
1、快速排序
选择数组的其中一个元素(一般为第一个)作为分界pivot,用两个游标分别从后往前和从前往后扫描数组。先从后游标开始,当后游标所指的值比pivot小,则与pivot交换,后游标交换后才扫描前游标;当前游标所指值比pivot大,则与pivot交换。一次分组的结果是pivot前面的元素全部比pivot小,后面的全部比pivot大。既然对前后两部分继续调用分组函数即可完成排序。
下面的程序对上述过程做了优化,交换的时候直接把游标所指的值覆盖到pivot的位置上,覆盖后,原来游标所指的位置作为下一次的pivot位置,准备被下一次调换时被覆盖。当前后两个游标相遇时,此位置就是pivot值在有序数组中的位置了。此优化其实就是利用了pivot的位置进行元素交换,避免了使用多余的空间。
int partition(int *p,int begin,int end){ int ipivot=begin; int pivot=p[begin]; begin++; while(begin<=end){ while(begin<=end && p[end]>=pivot){ end--; } if(begin<=end){ p[ipivot]=p[end]; ipivot=end; end--;//这里不用忘记了 } while(begin<=end && p[begin]<=pivot){ begin++; } if(begin<=end){ p[ipivot]=p[begin]; ipivot=begin; begin++;//这里不用忘记了 } } p[ipivot]=pivot; return ipivot; } void myqs(int *p,int begin,int end){ if(begin>=end){ return; } int ipivot=partition(p,begin,end); myqs(p,begin,ipivot-1); myqs(p,ipivot+1,end); }
2、完全二叉树的判断、二叉树的按层遍历
完全二叉树:给二叉树按层编号,如果二叉树的编号与满二叉树的编号一一对应(但节点数比满二叉树少),就称为完全二叉树。通俗来说就是叶子节点都集中在树的左侧。
性质:除最底层外,上层是一棵满二叉树。不可能单独出现右叶节点。利用队列可按层遍历二叉树,遍历过程中如发现叶节点或只有左儿子的节点,则后面的节点都只能为叶子节点,否则不是完全二叉树。如发现节点只有右儿子,则不是完全二叉树。
按层遍历二叉树:从根节点开始,压根节点进队。当队列非空,把队头节点出队(浏览节点数据),把节点左右儿子入队。不断重复此操作至队列为空。
void Tree::show_tree_bylevel(){ queue<BinaryTreeNode*> que; que.push(m_pRoot); while(!que.empty()){ cout<<que.front()->m_nValue<<endl; if(que.front()->m_pLeft!=NULL)que.push(que.front()->m_pLeft); if(que.front()->m_pRight!=NULL)que.push(que.front()->m_pRight); que.pop(); } } int Tree::is_complete_binarytree(){ queue<BinaryTreeNode*> que; int flag=0; que.push(m_pRoot); while(!que.empty()){ BinaryTreeNode *p=que.front(); if(flag && (p->m_pLeft!=NULL || p->m_pRight!=NULL))return 0;//标记后只能出现叶子节点 if(p->m_pLeft==NULL && p->m_pRight!=NULL)return 0;//有单右儿子,不是完全树 if(p->m_pRight==NULL)flag=1;//出现单左或者叶子节点,则标记 if(p->m_pLeft!=NULL)que.push(p->m_pLeft); if(p->m_pRight!=NULL)que.push(p->m_pRight); que.pop(); } return 1; }
3、普通插入排序与希尔排序
插入排序(insert sort):从数组头往后扫描,发现非升序(降序)的元素时,先记录于buf,把前面的元素往后移动,直到遇到比buf小的元素,则把buf放到此元素后面。复杂度O(n^2)
希尔排序(shell sort):是插入排序的改进。与插入排序不同的是,它把原数组按一定的gap分组,在分组内进行插入排序。逐渐把分组的间隔缩小,最后gap=1时就相当于进行普通的插入排序。因为组内的元素间隔为gap,所以元素需要移动时可以比普通插入排序(即gap=1)时更快地移动,从而提高效率.
如 4,3,6,2,65,1,7,8 以gap=4: 4 65;3 1;6 7;2 8 因此1后移到3的位置,其余不需要移动,结果为:4,1,6,2,65,3,7,8 gap=2:4 6 65 7;1 2 3 8 因此7后移到65之后,第二个分组不需移动,结果为:4,1,6,2,7,3,65,8 gap=1:普通的插入排序,把后面的元素往前面的有序部分插入
void insert_sort(int *p,int n){ for(int i=1;i<n;++i){ if(p[i-1]>p[i]){ int buf=p[i];//暂存需要插入前方的数据 int j; for(j=i-1;p[j]>buf && j>=0;--j)//数据往后移,直到buf的合适位置 p[j+1]=p[j]; p[j+1]=buf;//最后一次操作j多减了1 } } } void shell_sort(int *p,int n){ for(int gap=n/2;gap>0;gap/=2){//最后一次gap=1,以一个普通的插入排序结束 for(int i=gap;i<n;i++){ if(p[i-gap]>p[i]){ int buf=p[i];//暂存需要插入前方的数据 int j; for(j=i-gap;p[j]>buf && j>=0;j-=gap)//数据以gap速度移动,较插入排序快 p[j+gap]=p[j]; p[j+gap]=buf;//最后一次操作j多减了1 } } } }
4、实现strstr函数
char* strstr(char *str1,const char *str2){//查找str2,如有则从受托人str2位置开始返回 /*string target=str1; string::size_type k=target.find(str2); return str1+k;*/ size_t len=strlen(str2); while(*str1!='\0'){ if(strncmp(str1,str2,len)==0){//有n不对比到\0,对比len个 return str1; } str1++; } return NULL; }
5、一句话判断x是否为2的若干次幂
int f(int x){//由于要求一句话,很多地方没考虑,例如输入0也会返回1 return (x&(x-1))?0:1; }
6、辗转相处求最大公约数
如余数为0则被除数为最大公约数;否则以被除数除以余数进行递归。
int max_gys(int a,int b){ /*while(true){//非递归 if(a%b==0)return b; int tmp=a%b; a=b; b=tmp; } return 0;*/ if(a%b==0)return b; else return max_gys(b,a%b); }
6、后缀式(逆波兰式)求四则混合运算
表达式转化为后缀式:1、若数字则直接输出;2、若左括号和优先级比栈顶符号更高(不含相同优先级)的符号,则直接进栈;3、若右括号,则输出栈中左括号以上的;4、若优先级不高于(小于等于)栈顶,则输出直到遇到优先级更高的符号(括号的优先级算最低,比加减乘除低)为止。
int is_higher(char a,char b){//*/优先级高于+-,括号优先级低于所有符号 if((a=='*' || a=='/') && (b=='+' || b=='-'))return 1; else if(b=='(' || b==')')return 1; else return 0; } /*普通中缀表达式转换为后缀表达式*/ void backword(char *mid,char *back){ stack<char> sign; while(*mid!='\0'){ if(*mid>='0' && *mid<='9')*back++=*mid++;//数字 else if(*mid==')'){//右括号 while(sign.top()!='('){ *back++=sign.top(); sign.pop(); } sign.pop(); mid++; }else if(*mid=='('){//左括号 sign.push(*mid++); }else if(!sign.empty() && is_higher(*mid,sign.top())){//优先级比栈顶高或左括号 sign.push(*mid++); }else{//优先级不高于 while(!sign.empty() && !is_higher(*mid,sign.top())){ *back++=sign.top(); sign.pop(); } sign.push(*mid++); } } while(!sign.empty()){ *back++=sign.top(); sign.pop(); } *back='\0'; }
计算后缀式:遇到符号则出栈两个数字计算符号,结果进栈。
/*计算后缀式*/ int get_result(char *back){ stack<int> num; while(*back!='\0'){ if(*back>='0' && *back<='9'){ num.push(*back-'0'); }else{ int right=num.top(); num.pop(); int left=num.top(); num.pop(); if(*back=='+') num.push(left+right); else if(*back=='-') num.push(left-right); else if(*back=='*') num.push(left*right); else if(*back=='/') num.push(left/right); } back++; } return num.top(); }
7、按位逆序一个32位长的整数,如110000.。。变为0000.。。11
//位操作 void bit_set(int &b,int n){ b|=(1<<n); } void bit_clear(int &b,int n){ b&=~(1<<n); } int bit_check(int &b,int n){ return b&(1<<n)?1:0; } void bit_reverse(int &b,int n){ b^=(1<<n); } int main() { //逆序32位长整数 int b=0; bit_set(b,31); bit_set(b,30); cout<<hex<<b<<endl; int r=0; for(int i=31;i>=0;--i){ if(bit_check(b,i)) bit_set(r,31-i); } cout<<hex<<r<<endl; return 0; }
8、倒水问题:给两个容量不同的容器,容器之间可以相互倒水,求能否最终量出容量为n的水
//问题的实质是,最终要求的容量能否整除两容器的最小公因数 int gdc(int a,int b){//使用辗转相除法求最小公因数 if(b>a){ a^=b; b^=a; a^=b; } while(b!=0){ a=a%b; if(b>a){ a^=b; b^=a; a^=b; } } return a; } bool can(int a,int b,int c){ int g=gdc(a,b); if(c%g==0) return true; else return false; }
9、二分查找
int binary_search_july(int array[],int n,int value) { int left=0; int right=n-1; //如果这里是int right = n 的话,那么下面有两处地方需要修改,以保证一一对应: //1、下面循环的条件则是while(left < right) //2、循环内当array[middle]>value 的时候,right = mid while (left<=right) //循环条件,适时而变 { int middle=left + ((right-left)>>1); //防止溢出,移位也更高效。同时,每次循环都需要更新。 if (array[middle]>value) { right =middle-1; //right赋值,适时而变 } else if(array[middle]<value) { left=middle+1; } else return middle; //可能会有读者认为刚开始时就要判断相等,但毕竟数组中不相等的情况更多 //如果每次循环都判断一下是否相等,将耗费时间 } return -1; } int july_bsearch_recur(int *p,int left,int right,int v){ if(right<left){ return -1; } int mid=left+((right-left)>>1);//使用减法不会溢出,移位效率高 if(p[mid]<v){ return july_bsearch_recur(p,mid+1,right,v);//递归时注意参数完整 }else if(p[mid]>v){ return july_bsearch_recur(p,left,mid-1,v); }else return mid; }
二分查找需要注意的坑还是挺多的
1、不要改变p,因此需要定义一个begin和end确定二分的范围,一旦改变了p,最后的返回值将是错误的。
2、mid=left+((right-left)>>1);//使用减法不会溢出,移位效率高
3、编程过程中一定要区分相对数组头p的偏移量(返回值),和相对于begin的偏移量,绝对不能混淆
4、使用right<left这种形式判断结束标志,可以防止左右越界
10、约瑟夫环,有1到n个人排成一圈,从第k个人开始从1开始数到m,数到m的人出列,直到没人,求出列顺序
void yuesefu(int n,int k,int m){ list<int> v; int i; for(i=1;i<=n;++i){ v.push_back(i); } list<int>::iterator it=v.begin(); for(i=0;i<k-1;++i){ it++; if(it==v.end()){ it=v.begin(); } } while(n>0){ int j=1; while(j<=m){ if(it==v.end()){ it=v.begin(); } if(j==m){ cout<<*it<<" "; it=v.erase(it); n--; break; } it++; j++; } } cout<<endl; }
1、使用链表存放一个从1到n 2、把迭代器移到k的位置 3、循环移动,出列
注意,当it到达v.end()的之后,要令it=v.begin()达到循环目的
使用循环链表
void ysf(int n,int k,int m){ node *buf=new node[n]; int i; for(i=1;i<=n;++i){//构造循环链表 buf[i-1].val=i; if(i!=n) buf[i-1].next=&buf[i]; else buf[i-1].next=&buf[0]; } node *p=&buf[k-1],*pre; while(p->next!=p){//使用这个判断,最后一个不会在循环内输出 for(i=0;i<m-1;++i){ pre=p; p=p->next; } cout<<p->val<<" "; pre->next=p->next; p=p->next; } cout<<p->val<<" ";//记得 最后一个还没输出! cout<<endl; delete []buf; }
11、链地址法hash
本例子的哈希函数只选用简单的求模,遇到冲突时使用链地址法。注意查找时要先检查链上的key有无和当前插入的key一样的,有的话直接修改value,不需插入节点。
class hash { public: link* table[10]; void put(int key,int val); int get(int key); hash(){ memset(table,0,10*sizeof(link*)); } }; void hash::put(int key,int val){ int i=key%10; link *p=table[i]; if(!p){ table[i]=new link; table[i]->key=key; table[i]->value=val; table[i]->pnext=NULL; return; } while(p){ if(p->key==key){//检查是否已存在key p->value=val; return; } if(p->pnext==NULL){ p->pnext=new link; p->pnext->key=key; p->pnext->value=val; p->pnext->pnext=NULL; } p=p->pnext; } } int hash::get(int key){ int i=key%10; link *p=table[i]; while(p){ if(p->key==key){//检查是否已存在key return p->value; } p=p->pnext; } return -1; }