网上摘录,想分专题放到一起。数据结构常见的问题包括字符串方面、链表的各种操作、树的各种操作,以及各种变形和与其它数据结构的结合使用。
面试题目 字符串专题
1.将字符串转换成整数,将整数转换为字符串,浮点数与字符串的转换(atoi itoa)
int atoi(const char *str){
int res = 0;
int sign;
assert(str != NULL);
if(str[0] == '-') sign = -1;
else if(str[0] == '+') sign =1;
else if(isdigit(str[0])){sign = 1;res=str[0] - '0';}
for(int i = 1 ; str[i] != '\0' ; i++){
assert(isdigit(str[i]));
res = res*10 + sign*(str[i] - '0');
}
return res;
}
别看一个这么简单的问题,实际要考虑的问题很多。还是看一下glibc的实现吧
#define LONG_MAX 2147483647L
#define LONG_MIN (-2147483647L-1L)
long int _strtol_internal (const char *nptr, char **endptr, int base, int group)
{
//注意要使用unsigned long否则会发生溢出,因为long int最多2147483647L ,无法表示2147483648L
unsigned long int result = 0;
long int sign = 1;
//考虑前导空格
while (*nptr == ' ' || *nptr == '\t')
++nptr;
//考虑带有正负号
if (*nptr == '-')
{
sign = -1;
++nptr;
}
else if (*nptr == '+')
++nptr;
//如果出现非法输入
if (*nptr < '0' || *nptr > '9')
{
if (endptr != NULL)
*endptr = (char *) nptr;
return 0L;
}
//考虑进制
assert (base == 0);
base = 10;
if (*nptr == '0')
{
if (nptr[1] == 'x' || nptr[1] == 'X')
{
base = 16;
nptr += 2;
}
else
base = 8;
}
//防止非法字符
while (*nptr >= '0' && *nptr <= '9')
{
unsigned long int digval = *nptr - '0';
//防止溢出,如果溢出了long的表示范围,则置errno
if (result > LONG_MAX / 10 || (sign > 0 ? result == LONG_MAX / 10 && digval > LONG_MAX % 10 :
(result == ((unsigned long int) LONG_MAX + 1) / 10 && digval > ((unsigned long int) LONG_MAX + 1) % 10)))
{
errno = ERANGE;
return sign > 0 ? LONG_MAX : LONG_MIN;
}
result *= base;
result += digval;
++nptr;
}
return (long int) result * sign;
}
atoi函数就是这个函数讲第二个参数置为NULL,第三个参数置为10。不知道你注意到了那些空格,越界之类的判断没有。我同学说他写出来的代码最后就被要求加上了这些东西,最后还因此被卡掉了(
说是考虑不够慎密,汗)。
2.找出字符串的最长子串,要求子串的所有字符相同
int find_sub(char *str,char **begin,char **end){
unsigned int len = 1;
unsigned int max_len = 0;
char *t_begin = str;
char *t_end = str;
if(*str == '\0')return 0;
//注意起始条件,与循环都要保证循环不变形,可以这样考虑第一次循环前执行的,与while的其他循环是否有不同
//比如在while里,其他循环执行都有个str++,而第一次是否需要加上这个str++呢?
str++;
while(*str){
if(*str == *t_end){ len++;t_end = str;}
else{
if(len > max_len){
max_len = len;
*begin = t_begin;
*end = t_end;
}
len = 1;
t_begin = str;
t_end = str;
}
str++;
}
return max_len;
}
注意检查逻辑错误,边界条件。
3.求两个字符串的最大公共子字符串
dp,设两个字符串为A B定义子问题f[i][j]表示子串A[0...i] B[0...j]以i,j为结尾的最大公关子串长度。
f[i][j] = 0 if(A[i] != B[j])
= f[i-1][j-1]+1 else
这样可以找到复杂度为O(n^2)的算法
4.字符串查找并记录出现次数(普通与kmp)(观察strstr实现),替代
看下标准实现,实际上是还是用的force-brute方法,并没有使用kmp这类的算法。看下lynx里的strstr.c
/* Written by Philippe De Muyter <>. */
/*
* NAME
*
* strstr -- locate first occurrence of a substring
*
* SYNOPSIS
*
* char *strstr (char *s1, char *s2)
*
* DESCRIPTION
*
* Locates the first occurrence in the string pointed to by S1 of the string
* pointed to by S2. Returns a pointer to the substring found, or a NULL
* pointer if not found. If S2 points to a string with zero length, the
* function returns S1.
*
* BUGS
*
*/
char *strstr(char *buf, char *sub)
{
register char *bp;
register char *sp;
if (!*sub)
return buf;
while (*buf) {
bp = buf;
sp = sub;
do {
if (!*sp)
return buf;
} while (*bp++ == *sp++);
buf += 1;
}
return 0;
}
5.解析一个字符串,对字符串中重复出现的字符,只在第一次出现时保留。如:abdabbefgf -> abdefg。
根据字符集,建立一个flag数组用来表示是否出现过。
6.给出一个函数来输出一个字符串的所有排列
采用next_permutation的算法思想,首先进行一个字符重排序找到按字典序最小的那个字符序列,以它为开端逐步生成所有排列。
7.翻转字符串
这个比较简单。
8.从一个字符串中找出第一个不重复字符
这个也比较简单,类似于5的方法。
9.去除字符串中相邻两个字符的重复
这个应该等价于题目5
10.判断字符串是否含有回文字符子串
枚举字符串的每个位置,作为回文串的中间位置(分偶数奇数两种情况),如果找到或者找不到都会立即停止,所以总的复杂度不超过O(n)
11.求最长回文子串
当然这里有一个小小的限定,f[i][j]表示以i,j为首尾的回文串能否构成。然后再找到一个最长的就可以算法,复杂度O(n^2)。实际上这个问题只要枚举回文串的中间位置就可以了,这样实际上就跟10一样了。不过10只需判断是否存在,这需要找到最长的那个。
------------------------------------------------------引用开始------------------------------------------------------------------------
KMP的另外一个研究方向是Extend KMP(以下简称EK),它是说求得T与所有的S(i)的最长公共前缀(LCP),当然,要控制复杂度在线性以内。
EK我第一次听说是07年baidu校园招聘的笔试题中,它当时的题目是求最长回文子串,当然这是一个耳熟能详,路人皆知可以用Suffix Array很好解决的问题。事后听一个同学说他写了三个算法:Suffix Array,Suffix Tree和EK,当时就不明白EK是什么东西,但又没当面问他,于是这个东西就搁置了很久。知道后来北大的月赛一道题说可以用EK来做,我才终于从03年林希德的文章中开始认识到它,就像KMP一样,这个算法也一下就吸引了我。
设Q(i)表示T和S(i )的后缀的LCP,P(i)表示T和T(i)的后缀的LCP,那么和KMP一样,我们试图用P来求得Q,而P可以用自匹配求得,并且和求Q的过程相似。
我以求P为例简要说明一下。P(2)就直接匹配即可,从i = 3开始,如下:
设k < i,E(k) = k + P(k) - 1,且对所有j < i,有E(k) >= E(j)。
那么,当E(k) >= i时,便可以推知T(i) = T( i - k + 1 ),于是如果P( i - k + 1 ) < E(k) - i + 1,那么P(i) = P( i - k + 1),否则P(i) >= P( i - k + 1 ),从E(k)开始向后匹配到E(i),有P(i) = E(i) - i + 1,并且更新 k = i;
还有就是E(k) < i,肯定有E(k) = i - 1,不过这个不重要,重要的是直接从i开始做暴力匹配即可得到E(i),则P(i) = E(i) - i + 1,更新k = i。
希望我把EK说清楚了,不过这种东西还是自己推导一下有意思,而且记忆周期更长。
最后来罗列下题目。KMP的经典题目是POJ 2185,是要找最小覆盖矩形,如果你认为懂KMP了就去尝试它。EK的经典题是POJ 3376,有一定挑战;当然还有就是上面说的最长回文子串,提醒下用分治+EK来做是其中一种方法。
嗯,下次打算说下Suffix Array,主要是它那个传说中的线性DC3算法,不过我现在还没把握能不能简单的把它说清楚,姑且认为可以吧。
------------------------------------------------------引用结束------------------------------------------------------------------------
12. 字符串移位包含的问题 给定两个字符串S1与S2,要求判定S2是否能够被通过s1作循环移位得到的字符串包含,例如,给定s1=AABCD与S2=CDAA,返回true;给定s1=ABCD 与 s2=ACBD,返回false.
直接枚举匹配或者比较s2能否匹配s1s1,以第一个为例也就是说比较AABCDAABCD和CDAA。
13.strlen strcpy(注意重叠)
看glibc的实现,可以发现strlen它进行了加速处理,每次前进多个字节,将字符串转化为unsigned long类型,然后利用位运算判断该long型数据是否含有0字节。
strcpy的实现:
# define BOUNDS_VIOLATED (__builtin_trap (), 0)
# define CHECK_BOUNDS_LOW(ARG) \
(((__ptrvalue (ARG) < __ptrlow (ARG)) && BOUNDS_VIOLATED), \
__ptrvalue (ARG))
# define CHECK_BOUNDS_HIGH(ARG) \
(((__ptrvalue (ARG) > __ptrhigh (ARG)) && BOUNDS_VIOLATED), __ptrvalue (ARG))
char *
strcpy (
char *dest,
const char *src
)
{
reg_char c;
char *__unbounded s = (char *__unbounded) CHECK_BOUNDS_LOW (src);
const ptrdiff_t off = CHECK_BOUNDS_LOW (dest) - s - 1;
size_t n;
n = s - src;
(void) CHECK_BOUNDS_HIGH (src + n);
(void) CHECK_BOUNDS_HIGH (dest + n);
上面这个比较复杂,再看FreeBsd的实现
sys/libkern/strcpy.c
35 char *
36 strcpy(char * __restrict to, const char * __restrict from)
37 {
38 char *save = to;39
40 for (; (*to = *from) != 0; ++from, ++to);
41 return(save);
42 }
14.去掉中文中的英文字符
主要根据字节的第一位进行判断。
15.求一个字符串中连续出现次数最多的子串
【字符串】
1、输入一个字符串,打印出该字符串中字符的所有排列。
例如输入字符串abc,则输出由字符a、b、c所能排列出来的所有字符串abc、acb、bac、bca、cab和cba。
2、有一个由大小写组成的字符串,现在需要对他进行修改,将其中的所有小写字母排在大写字母的前面
(大写或小写字母之间不要求保持原来次序),如有可能尽量选择时间和空间效率高的算法。
c语言函数原型void proc(char *str),也可以采用你自己熟悉的语言。
3、编写反转字符串的程序,要求优化速度、优化空间。
4、用C语言实现函数void *memmove(void *dest, const void *src, size_t n)。
memmove函数的功能是拷贝src所指的内存内容前n个字节到dest所指的地址上。
分析:由于可以把任何类型的指针赋给void类型的指针,这个函数主要是实现各种数据类型的拷贝。
5、编程找出两个字符串中最大公共子字符串,如"abccade","dgcadde"的最大子串为"cad"。
6、输入一个字符串,输出该字符串中对称的子字符串的最大长度。
比如输入字符串"google",由于该字符串里最长的对称子字符串是"goog",因此输出4。
7、字符串原地压缩。题目描述:“eeeeeaaaff" 压缩为 "e5a3f2",请编程实现。
8、请以回溯与不回溯算法实现字符串匹配。
9、输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。句子中单词以空格符隔开。
为简单起见,标点符号和普通字母一样处理。
例如:输入"Iam a student.",则输出"student. a am I"。
10、在一个字符串中找到第一个只出现一次的字符。如输入abaccdeff,则输出b。
11、写一个函数,它的原形是int continumax(char *outputstr,char *intputstr)
功能:
在字符串中找出连续最长的数字串,并把这个串的长度返回,并把这个最长数字串付给其中一个函数参数outputstr所指内存。
例如:"abcd12345ed125ss123456789"的首地址传给intputstr后,函数将返回9,outputstr所指的值为123456789。
12、定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。
如:把字符串abcdef左旋转2位得到字符串cdefab。请实现字符串左旋转的函数。
要求时间对长度为n的字符串操作的复杂度为O(n),辅助内存为O(1)。
13、有n个长为m+1的字符串,如果某个字符串的最后m个字符与某个字符串的前m个字符匹配,则两个字符串可以联接。
问这n个字符串最多可以连成一个多长的字符串,如果出现循环,则返回错误。
14、如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中,则字符串一称之为字符串二的子串。
注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。
请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。
例如:输入两个字符串BDCABA和ABCBDAB,字符串BCBA和BDAB都是是它们的最长公共子串,
则输出它们的长度4,并打印任意一个子串。
分析:求最长公共子串(LongestCommon Subsequence, LCS)是一道非常经典的动态规划题。
15、输入两个字符串,从第一字符串中删除第二个字符串中所有的字符。
例如,输入"Theyare students."和"aeiou",则删除之后的第一个字符串变成"Thy r stdnts."。
16、一个文件,内含一千万行字符串,每个字符串在1K以内,要求找出所有相反的串对,如abc和cba。
17、给出一个函数来复制两个字符串A和B。字符串A的后几个字节和字符串B的前几个字节重叠。
18、已知一个字符串,比如asderwsde,寻找其中的一个子字符串比如sde的个数,如果没有返回0,有的话返回子字符串的个数。
19、求最大连续递增数字串(如"ads3sl456789DF3456ld345AA"中的"456789")。
20、实现strstr功能,即在父串中寻找子串首次出现的位置。
21、编码完成下面的处理函数。
函数将字符串中的字符'*'移到串的前部分,前面的非'*'字符后移,但不能改变非'*'字符的先后顺序,函数返回串中字符'*'的数量。
如原始串为:ab**cd**e*12,处理后为*****abcde12,函数并返回值为5。(要求使用尽量少的时间和辅助空间)
22、删除字符串中的数字并压缩字符串。如字符串”abc123de4fg56”处理后变为”abcdefg”。注意空间和效率。
23、求两个串中的第一个最长子串(神州数码以前试题)。如"abractyeyt","dgdsaeactyey"的最大子串为"actyet"。
【栈、链表、树、图】
1、编写一个程序,把一个有序整数数组放到二叉树中。
2、编程实现从顶部开始逐层打印二叉树节点数据。[参考]
3、编程实现单链表逆转。
4、设计一个算法,找出二叉树上任意两个结点的最近共同父结点。复杂度不能为O(n2)。
5、二叉排序树中,令f = (最大值+最小值) / 2,设计一个算法,找出距离f值最近、大于f值的结点。复杂度不能为O(n2)。
6、有双向循环链表结点定义为:
struct node
{
int data;
struct node *front,*next;
};
有两个双向循环链表A,B,知道其头指针为:pHeadA、pHeadB,请写一函数将两链表中data值相同的结点删除。
7、输入一个链表的头结点,从尾到头反过来输出每个结点的值。链表结点定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
8、输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。
要求不能创建任何新的结点,只调整指针的指向。
10
/ \
6 14
/ \ / \
4 8 12 16
转换成双向链表
4=6=8=10=12=14=16。
首先我们定义的二元查找树 节点的数据结构如下:
struct BSTreeNode
{
int m_nValue; // value of node
BSTreeNode *m_pLeft; // left child of node
BSTreeNode *m_pRight; // right child ofnode
}; [参考]
9、设计包含min函数的栈。
定义栈的数据结构,要求添加一个min函数,能够得到栈的最小元素。
要求函数min、push以及pop的时间复杂度都是O(1)。
10、输入一个整数和一棵二元树。从树的根结点开始往下访问一直到叶结点所经过的所有结点形成一条路径。
打印出和与输入整数相等的所有路径。
例如 输入整数22和如下二元树
10
/ \
5 12
/ \
4 7
则打印出两条路径:10,12和10, 5, 7。
二元树节点的数据结构定义为:
struct BinaryTreeNode // a node in thebinary tree
{
int m_nValue; // value of node
BinaryTreeNode *m_pLeft; // left child ofnode
BinaryTreeNode *m_pRight; // right child ofnode
};
11、给出俩个单向链表的头指针,比如h1,h2,判断这俩个链表是否相交。为了简化问题,我们假设俩个链表均不带环。
问题扩展:
(1) 如果链表可能有环列?
(2) 如果需要求出俩个链表相交的第一个节点列?
12、输入一个整数数组,判断该数组是不是某二元查找树的后序遍历的结果。
如果是返回true,否则返回false。
例如输入5、7、6、9、11、10、8,由于这一整数序列是如下树的后序遍历结果:
8
/ \
6 10
/ \ / \
5 7 9 11
因此返回true。
如果输入7、4、6、5,没有哪棵树的后序遍历的结果是这个序列,因此返回false。
13、如果我们把二叉树看成一个图,父子节点之间的连线看成是双向的,我们姑且定义"距离"为两节点之间边的个数。
写一个程序,求一棵二叉树中相距最远的两个节点之间的距离。
14、输入一个单向链表,输出该链表中倒数第k个结点。链表的倒数第0个结点为链表的尾指针。
链表结点定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
15、输入一颗二元查找树,将该树转换为它的镜像,即在转换后的二元查找树中,左子树的结点都大于右子树的结点。
用递归和循环两种方法完成树的镜像转换。
例如输入:
8
/ \
6 10
/\ /\
5 7 9 11
输出:
8
/ \
10 6
/ \/ \
11 97 5
定义二元查找树的结点为:
struct BSTreeNode
{
int m_nValue; // value of node
BSTreeNode *m_pLeft; // left child of node
BSTreeNode *m_pRight; // right child ofnode
}; [参考]
16、求一个二叉树中任意两个节点间的最大距离,两个节点的距离的定义是这两个节点间边的个数。
比如某个孩子节点和父节点间的距离是1,和相邻兄弟节点间的距离是2,优化时间空间复杂度。
17、求一个有向连通图的割点,割点的定义是,如果除去此节点和与其相关的边,有向图不再连通,描述算法。
18、设计一个栈结构,满足一下条件:min,push,pop操作的时间复杂度为O(1)。
19、请修改append函数,利用这个函数实现:
两个非降序链表的并集,1->2->3和2->3->5 并为1->2->3->5
另外只能输出结果,不能修改两个链表的数据。
20、递归和非递归俩种方法实现二叉树的前序遍历。
21、输入一棵二元树的根结点,求该树的深度。
从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
22、用俩个栈实现队列,某队列的声明如下:
template<typename T> class CQueue
{
public:
CQueue() {}
~CQueue() {}
void appendTail(const T& node); //append a element to tail
void deleteHead(); // remove a element fromhead
private:
T m_stack1;
T m_stack2;
};
23、给定链表的头指针和一个结点指针,在O(1)时间删除该结点。链表结点的定义如下:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
函数的声明如下:voidDeleteNode(ListNode* pListHead, ListNode* pToBeDeleted);
24、两个单向链表,找出它们的第一个公共结点。
链表的结点定义为:
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
25、用递归颠倒一个栈。例如输入栈{1, 2, 3, 4, 5},1在栈顶。颠倒之后的栈为{5,4, 3, 2, 1},5处在栈顶。
26、二叉树的结点定义如下:
struct TreeNode
{
int m_nvalue;
TreeNode* m_pLeft;
TreeNode* m_pRight;
};
输入二叉树中的两个结点,输出这两个结点在数中最低的共同父结点。
27、有一个复杂链表,其结点除了有一个m_pNext指针指向下一个结点外,还有一个m_pSibling指向链表中的任一结点或者NULL。
其结点的C++定义如下:
struct ComplexNode
{
int m_nValue;
ComplexNode* m_pNext;
ComplexNode* m_pSibling;
};
下图是一个含有5个结点的该类型复杂链表。
图中实线箭头表示m_pNext指针,虚线箭头表示m_pSibling指针。为简单起见,指向NULL的指针没有画出。
请完成函数ComplexNode*Clone(ComplexNode* pHead),以复制一个复杂链表。
28、给定单链表、检测是否有环。
29、给定两个单链表(head1, head2),检测两个链表是否有交点,如果有返回第一个交点。
30、给定单链表(head),如果有环的话请返回从头结点进入环的第一个节点。
31、只给定单链表中某个结点p(并非最后一个结点,即p->next!=NULL)指针,删除该结点。
32、只给定单链表中某个结点p(非空结点),在p前面插入一个结点。
33、编写实现链表排序的一种算法。说明为什么你会选择用这样的方法?
34、编写实现数组排序的一种算法。说明为什么你会选择用这样的方法?
35、怎样编写一个程序,把一个有序整数数组放到二叉树中?
【数组和集合】
1、有一个整数数组,请求出两两之差绝对值最小的值。
2、一个整数数列,元素取值可能是1~N(N是一个较大的正整数)中的任意一个数,相同数值不会重复出现。
设计一个算法,找出数列中符合条件的数对的个数,满足数对中两数的和等于N+1。复杂度不能为O(n2)。
3、给定一个集合A=[0,1,3,8](该集合中的元素都是在0,9之间的数字,但未必全部包含),指定任意一个正整数K,
请用A中的元素组成一个大于K的最小正整数。比如,A=[1,0] K=21 那么输出结构应该为100。
4、一个有序数列,序列中的每一个值都能够被2或者3或者5所整除,1是这个序列的第一个元素。求第1500个值是多少?
5、1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。
每个数组元素只能访问一次,设计一个算法,将它找出来,不用辅助存储空间。
6、一个含n个元素的整数数组至少存在一个重复数,请编程实现,在O(n)时间内找出其中任意一个重复数。
7、输入a1,a2,...,an,b1,b2,...,bn, 在O(n)的时间,O(1)的空间将这个序列顺序改为a1,b1,a2,b2,a3,b3,...,an,bn,
且不需要移动,通过交换完成,只需一个交换空间。
8、给定一个存放整数的数组,重新排列数组使得数组左边为奇数,右边为偶数。
要求:空间复杂度O(1),时间复杂度为O(n)。
9、输入一个整形数组,数组里有正数也有负数。数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。
求所有子数组的和的最大值。要求时间复杂度为O(n)。
例如:输入的数组为1,-2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,因此输出为该子数组的和18。
10、输入一个已经按升序排序过的数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。
要求:时间复杂度是O(n)。如果有多对数字的和等于输入的数字,输出任意一对即可。
例如:输入数组1、2、4、7、11、15和数字15。由于4+11=15,因此输出4和11。
11、有两个序列a,b,大小都为n,序列元素的值任意整数,无序;
要求:通过交换a,b中的元素,使[序列a元素的和]与[序列b元素的和]之间的差最小。
例如:
var a=[100,99,98,1,2, 3];
var b=[1, 2, 3, 4,5,40];
12、求一个矩阵中最大的二维矩阵(元素和最大)。如:
1 2 0 3 4
2 3 4 5 1
1 1 5 3 0
中最大的是:
4 5
5 3
要求:(1)写出算法;(2)分析时间复杂度;(3)用C写出关键代码。
13、一个整数数组,长度为n,将其分为m份,使各份的和相等,求m的最大值。
比如{3,2,4,3,6} 可以分成{3,2,4,3,6} m=1;
{3,6}{2,4,3} m=2
{3,3}{2,4}{6} m=3 所以m的最大值为3
14、求一个数组的最长递减子序列比如{9,4,3,2,5,4,3,2}的最长递减子序列为{9,5,4,3,2}
15、如何对n个数进行排序,要求时间复杂度O(n),空间复杂度O(1)。
16、输入一个正数n,输出所有和为n连续正数序列。
例如:输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以输出3个连续序列1-5、4-6和7-8。
17、给定一个存放整数的数组,重新排列数组使得数组左边为奇数,右边为偶数。
要求:空间复杂度O(1),时间复杂度为O(n)。
18、一个整型数组里除了两个数字之外,其他的数字都出现了两次。
请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
19、输入一个正整数数组,将它们连接起来排成一个数,输出能排出的所有数字中最小的一个。
例如输入数组{32,321},则输出这两个能排成的最小数字32132。
请给出解决问题的算法,并证明该算法。
20、把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个排好序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3, 4, 5,1, 2}为{1, 2, 3, 4,5}的一个旋转,该数组的最小值为1。
21、数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。
22、一个int数组,里面数据无任何限制,要求求出所有这样的数a[i],其左边的数都小于等于它,右边的数都大于等于它。
能否只用一个额外数组和少量其它空间实现。
23、在一个int数组里查找这样的数,它大于等于左侧所有数,小于等于右侧所有数。
24、求随机数构成的数组中找到长度大于=3的最长的等差数列,输出等差数列由小到大。
格式:
输入[1,3,0,5,-1,6]
输出[-1,1,3,5]
要求时间复杂度,空间复杂度尽量小。
25、递归法求数组中的最大值。[参考]
26、用递归的方法判断整数组a[N]是不是升序排列。
27、计算数组中连续元素和的最大值。[参考]
【其它】
1、1024!末尾有多少个0?[参考]
2、编程实现两个正整数的除法(不能用除法操作符)。
3、请定义一个宏,比较两个数a、b的大小,不能使用大于、小于、if语句。
4、两个数相乘,小数点后位数没有限制,请写一个高精度算法。
5、编程实现把十进制数(long型)分别以二进制和十六进制形式输出,不能使用printf系列。
6、输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
例如:如果输入如下矩阵:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
则依次打印出数字1, 2,3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10。
7、用1、2、2、3、4、5这六个数字,写一个main函数,打印出所有不同的排列。
如:512234、412345等,要求:"4"不能在第三位,"3"与"5"不能相连。
8、求两个或N个数的最大公约数和最小公倍数。
9、如果一个整数能够表示成两个或多个素数之和,则得到一个素数和分解式。
对于一个给定的整数,输出所有这种素数和分解式。
注意,对于同构的分解只输出一次(比如5只有一个分解2 + 3,而3 + 2是2 + 3的同构分解式)。
例如,对于整数8,可以作为如下三种分解:
(1) 8 = 2 + 2 + 2 + 2
(2) 8 = 2 + 3 + 3
(3) 8 = 3 + 5
10、输入n个整数,输出其中最小的k个。
例如输入1,2,3,4,5,6,7和8这8个数字,则最小的4个数字为1,2,3和4。
11、求1+2+…+n
要求不能使用乘除法、for、while、if、else、switch、case等关键字以及条件判断语句(A ? B : C)。
12、定义Fibonacci数列如下:
0 if n = 0
f(n)= 1 if n = 1
f(n-1)+f(n-2) if n >= 2
输入n,用最快的方法求该数列的第n项。[参考]
13、输入两个整数 n 和 m,从数列1,2,3.......n 中 随意取几个数,使其和等于m。
要求将其中所有的可能组合列出来。
14、输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。
15、对于一个整数矩阵,存在一种运算,对矩阵中任意元素加一时,需要其相邻(上下左右)某一个元素也加一。
现给出一正数矩阵,判断其是否能够由一个全零矩阵经过上述运算得到。
16、四对括号可以有多少种匹配排列方式?比如两对括号可以有两种:()()和(())
17、我们把只包含因子2、3和5的数称作丑数(Ugly Number)。
例如:6、8都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做是第一个丑数。
求按从小到大的顺序的第1500个丑数。
18、输入数字n,按顺序输出从1最大的n位10进制数。比如输入3,则输出1、2、3一直到最大的3位数即999。
19、大整数数相乘的问题。
=============================================================================================
目录
1.单链表反转
2.找出单链表的倒数第4个元素
3.找出单链表的中间元素
4.删除无头单链表的一个节点
5.两个不交叉的有序链表的合并
6.有个二级单链表,其中每个元素都含有一个指向一个单链表的指针。写程序把这个二级链表称一级单链表。
7.单链表交换任意两个元素(不包括表头)
8.判断单链表是否有环?如何找到环的“起始”点?如何知道环的长度?
9.判断两个单链表是否相交
10.两个单链表相交,计算相交点
11.用链表模拟大整数加法运算
12.单链表排序
13.删除单链表中重复的元素
首先写一个单链表的C#实现,这是我们的基石:
public class Link
{
public Link Next;
public string Data;
public Link(Link next, string data)
{
this.Next = next;
this.Data = data;
}
}
其中,我们需要人为地在单链表前面加一个空节点,称其为head。例如,一个单链表是1->2->5,如图所示:
对一个单链表的遍历如下所示:
static void Main(string[] args)
{
Link head = GenerateLink();
Link curr = head;
while (curr != null)
{
Console.WriteLine(curr.Data);
curr = curr.Next;
}
}
这道题目有两种算法,既然是要反转,那么肯定是要破坏原有的数据结构的:
算法1:我们需要额外的两个变量来存储当前节点curr的下一个节点next、再下一个节点nextnext:
public static Link ReverseLink1(Link head)
{
Link curr = head.Next;
Link next = null;
Link nextnext = null;
//if no elements or only one element exists
if (curr == null || curr.Next == null)
{
return head;
}
//if more than one element
while (curr.Next != null)
{
next = curr.Next; //1
nextnext = next.Next; //2
next.Next = head.Next; //3
head.Next = next; //4
curr.Next = nextnext; //5
}
return head;
}
算法的核心是while循环中的5句话
我们发现,curr始终指向第1个元素。
此外,出于编程的严谨性,还要考虑2种极特殊的情况:没有元素的单链表,以及只有一个元素的单链表,都是不需要反转的。
算法2:自然是递归
如果题目简化为逆序输出这个单链表,那么递归是很简单的,在递归函数之后输出当前元素,这样能确保输出第N个元素语句永远在第N+1个递归函数之后执行,也就是说第N个元素永远在第N+1个元素之后输出,最终我们先输出最后一个元素,然后是倒数第2个、倒数第3个,直到输出第1个:
public static void ReverseLink2(Link head)
{
if (head.Next != null)
{
ReverseLink2(head.Next);
Console.WriteLine(head.Next.Data);
}
}
但是,现实应用中往往不是要求我们逆序输出(不损坏原有的单链表),而是把这个单链表逆序(破坏型)。这就要求我们在递归的时候,还要处理递归后的逻辑。
首先,要把判断单链表有0或1个元素这部分逻辑独立出来,而不需要在递归中每次都比较一次:
public static Link ReverseLink3(Link head)
{
//if no elements or only one element exists
if (head.Next == null || head.Next.Next == null)
return head;
head.Next = ReverseLink(head.Next);
return head;
}
我们观测到:
head.Next = ReverseLink(head.Next);
这句话的意思是为ReverseLink方法生成的逆序链表添加一个空表头。
接下来就是递归的核心算法ReverseLink了:
static Link ReverseLink(Link head)
{
if (head.Next == null)
return head;
Link rHead = ReverseLink(head.Next);
head.Next.Next = head;
head.Next = null;
return rHead;
}
算法的关键就在于递归后的两条语句:
head.Next.Next = head; //1
head.Next = null; //2
啥意思呢?画个图表示就是:
这样,就得到了一个逆序的单链表,我们只用到了1个额外的变量rHead。
这道题目有两种算法,但无论哪种算法,都要考虑单链表少于4个元素的情况:
第1种算法,建立两个指针,第一个先走4步,然后第2个指针也开始走,两个指针步伐(前进速度)一致。
static Link GetLast4thOne(Link head)
{
Link first = head;
Link second = head;
for (int i = 0; i < 4; i++)
{
if (first.Next == null)
throw new Exception("Less than 4 elements");
first = first.Next;
}
while (first != null)
{
first = first.Next;
second = second.Next;
}
return second;
}
第2种算法,做一个数组arr[4],让我们遍历单链表,把第0个、第4个、第8个……第4N个扔到arr[0],把第1个、第5个、第9个……第4N+1个扔到arr[1],把第2个、第6个、第10个……第4N+2个扔到arr[2],把第3个、第7个、第11个……第4N+3个扔到arr[3],这样随着单链表的遍历结束,arr中存储的就是单链表的最后4个元素,找到最后一个元素对应的arr[i],让k=(i+1)%4,则arr[k]就是倒数第4个元素。
static Link GetLast4thOneByArray(Link head)
{
Link curr = head;
int i = 0;
Link[] arr = new Link[4];
while (curr.Next != null)
{
arr[i] = curr.Next;
curr = curr.Next;
i = (i + 1) % 4;
}
if (arr[i] == null)
throw new Exception("Less than 4 elements");
return arr[i];
}
本题目源代码下载:
推而广之,对倒数第K个元素,都能用以上2种算法找出来。
算法思想:类似于上题,还是使用两个指针first和second,只是first每次走一步,second每次走两步:
static Link GetMiddleOne(Link head)
{
Link first = head;
Link second = head;
while (first != null && first.Next != null)
{
first = first.Next.Next;
second = second.Next;
}
return second;
}
但是,这道题目有个地方需要注意,就是对于链表元素个数为奇数,以上算法成立。如果链表元素个数为偶数,那么在返回second的同时,还要返回second.Next也就是下一个元素,它俩都算是单链表的中间元素。
下面是加强版的算法,无论奇数偶数,一概通杀:
static void Main(string[] args)
{
Link head = GenerateLink();
bool isOdd = true;
Link middle = GetMiddleOne(head, ref isOdd);
if (isOdd)
{
Console.WriteLine(middle.Data);
}
else
{
Console.WriteLine(middle.Data);
Console.WriteLine(middle.Next.Data);
}
Console.Read();
}
static Link GetMiddleOne(Link head, refbool isOdd)
{
Link first = head;
Link second = head;
while (first != null && first.Next != null)
{
first = first.Next.Next;
second = second.Next;
}
if (first != null)
isOdd = false;
return second;
}
这道题目是典型的“狸猫换太子”,如下图所示:
如果不考虑任何特殊情况,代码就2行:
curr.Data = curr.Next.Data;
curr.Next = curr.Next.Next;
上述代码由一个地方需要注意,就是如果要删除的是最后一个元素呢?那就只能从头遍历一次找到倒数第二个节点了。
此外,这道题目的一个变身就是将一个环状单链表拆开(即删除其中一个元素),此时,只要使用上面那两行代码就可以了,不需要考虑表尾。
相关问题:只给定单链表中某个结点p(非空结点),在p前面插入一个结点q。
话说,交换单链表任意两个节点,也可以用交换值的方法。但这样就没意思了,所以,才会有第7题霸王硬上工的做法。
有两个有序链表,各自内部是有序的,但是两个链表之间是无序的。
算法思路:当然是循环逐项比较两个链表了,如果一个到了头,就不比较了,直接加上去。
注意,对于2个元素的Data相等(仅仅是Data相等哦,而不是相同的引用),我们可以把它视作前面的Data大于后面的Data,从而节省了算法逻辑。
static Link MergeTwoLink(Link head1, Linkhead2)
{
Link head = new Link(null, Int16.MinValue);
Link pre = head;
Link curr = head.Next;
Link curr1 = head1;
Link curr2 = head2;
//compare until one link run to the end
while (curr1.Next != null && curr2.Next != null)
{
if (curr1.Next.Data < curr2.Next.Data)
{
curr = new Link(null,curr1.Next.Data);
curr1 = curr1.Next;
}
else
{
curr = new Link(null, curr2.Next.Data);
curr2 = curr2.Next;
}
pre.Next = curr;
pre = pre.Next;
}
//if head1 run to the end
while (curr1.Next != null)
{
curr = new Link(null, curr1.Next.Data);
curr1 = curr1.Next;
pre.Next = curr;
pre = pre.Next;
}
//if head2 run to the end
while (curr2.Next != null)
{
curr = new Link(null, curr2.Next.Data);
curr2 = curr2.Next;
pre.Next = curr;
pre = pre.Next;
}
return head;
}
如果这两个有序链表交叉组成了Y型呢,比如说:
这时我们需要先找出这个交叉点(图中是11),这个算法参见第9题,我们这里直接使用第10道题目中的方法GetIntersect。
然后局部修改上面的算法,只要其中一个链表到达了交叉点,就直接把另一个链表的剩余元素都加上去。如下所示:
static Link MergeTwoLink2(Link head1, Linkhead2)
{
Link head = new Link(null, Int16.MinValue);
Link pre = head;
Link curr = head.Next;
Link intersect = GetIntersect(head1, head2);
Link curr1 = head1;
Link curr2 = head2;
//compare until one link run to the intersect
while (curr1.Next != intersect && curr2.Next != intersect)
{
if (curr1.Next.Data < curr2.Next.Data)
{
curr = new Link(null, curr1.Next.Data);
curr1 = curr1.Next;
}
else
{
curr = new Link(null, curr2.Next.Data);
curr2 = curr2.Next;
}
pre.Next = curr;
pre = pre.Next;
}
//if head1 run to the intersect
if (curr1.Next == intersect)
{
while (curr2.Next != null)
{
curr = new Link(null, curr2.Next.Data);
curr2 = curr2.Next;
pre.Next = curr;
pre = pre.Next;
}
}
//if head2 run to the intersect
else if (curr2.Next == intersect)
{
while (curr1.Next != null)
{
curr = new Link(null, curr1.Next.Data);
curr1 = curr1.Next;
pre.Next = curr;
pre = pre.Next;
}
}
return head;
}
这个简单,就是说,这个二级单链表只包括一些head:
public class Link
{
public Link Next;
public int Data;
public Link(Link next, int data)
{
this.Next = next;
this.Data = data;
}
}
public class CascadeLink
{
public Link Next;
public CascadeLink NextHead;
public CascadeLink(CascadeLink nextHead, Link next)
{
this.Next = next;
this.NextHead = nextHead;
}
}
下面做一个二级单链表,GenerateLink1和GenerateLink2方法在前面都已经介绍过了:
public static CascadeLinkGenerateCascadeLink()
{
Link head1 = GenerateLink1();
Link head2 = GenerateLink2();
Link head3 = GenerateLink1();
CascadeLink element3 = new CascadeLink(null, head3);
CascadeLink element2 = new CascadeLink(element3, head2);
CascadeLink element1 = new CascadeLink(element2, head1);
CascadeLink head = new CascadeLink(element1, null);
return head;
}
就是说,这些单链表的表头head1、head2、head3、head4……,它们组成了一个二级单链表head:null –> head1 –> head2 –> head3 –> head4
–>
我们的算法思想是: 进行两次遍历,在外层用curr1遍历二级单链表head,在内层用curr2遍历每个单链表:
public static LinkGenerateNewLink(CascadeLink head)
{
CascadeLink curr1 = head.NextHead;
Link newHead = curr1.Next;
Link curr2 = newHead;
while (curr1 != null)
{
curr2.Next = curr1.Next.Next;
while (curr2.Next != null)
{
curr2 = curr2.Next;
}
curr1 = curr1.NextHead;
}
return newHead;
}
其中,curr2.Next= curr1.Next.Next; 这句话是关键,它负责把上一个单链表的表尾和下一个单链表的非空表头连接起来。
先一次遍历找到这两个元素curr1和curr2,同时存储这两个元素的前驱元素pre1和pre2。
然后大换血
public static Link SwitchPoints(Link head,Link p, Link q)
{
if (p == head || q == head)
throw new Exception("No exchange with head");
if (p == q)
return head;
//find p and q in the link
Link curr = head;
Link curr1 = p;
Link curr2 = q;
Link pre1 = null;
Link pre2 = null;
int count = 0;
while (curr != null)
{
if (curr.Next == p)
{
pre1 = curr;
count++;
if (count == 2)
break;
}
else if (curr.Next == q)
{
pre2 = curr;
count++;
if (count == 2)
break;
}
curr = curr.Next;
}
curr = curr1.Next;
pre1.Next = curr2;
curr1.Next = curr2.Next;
pre2.Next = curr1;
curr2.Next = curr;
return head;
}
注意特例,如果相同元素,就没有必要交换;如果有一个是表头,就不交换。
算法思想:
先分析是否有环。为此我们建立两个指针,从header一起向前跑,一个步长为1,一个步长为2,用while(直到步长2的跑到结尾)检查两个指针是否相等,直到找到为止。
static bool JudgeCircleExists(Link head)
{
Link first = head; //1 step eachtime
Link second = head; //2 steps each time
while (second.Next != null && second.Next.Next != null)
{
second = second.Next.Next;
first = first.Next;
if (second == first)
return true;
}
return false;
}
那又如何知道环的长度呢?
根据上面的算法,在返回true的地方,也就是2个指针相遇处,这个位置的节点P肯定位于环上。我们从这个节点开始先前走,转了一圈肯定能回来:
static int GetCircleLength(Link point)
{
int length = 1;
Link curr = point;
while (curr.Next != point)
{
length++;
curr = curr.Next;
}
return length;
}
继续我们的讨论,如何找到环的“起始”点呢?
延续上面的思路,我们仍然在返回true的地方P,计算一下从有环单链表的表头head到P点的距离
static int GetLengthFromHeadToPoint(Linkhead, Link point)
{
int length = 1;
Link curr = head;
while (curr != point)
{
length++;
curr = curr.Next;
}
return length;
}
如果我们把环从P点“切开”(当然并不是真的切,那就破坏原来的数据结构了),那么问题就转化为计算两个相交“单链表”的交点(第10题):
一个单链表是从P点出发,到达P(一个回圈),距离M;另一个单链表从有环单链表的表头head出发,到达P,距离N。
我们可以参考第10题的GetIntersect方法并稍作修改。
private static Link FindIntersect(Linkhead)
{
Link p = null;
//get the point in the circle
bool result = JudgeCircleExists(head, ref p);
if (!result) return null;
Link curr1 = head.Next;
Link curr2 = p.Next;
//length from head to p
int M = 1;
while (curr1 != p)
{
M++;
curr1 = curr1.Next;
}
//circle length
int N = 1;
while (curr2 != p)
{
N++;
curr2 = curr2.Next;
}
//recover curr1 & curr2
curr1 = head.Next;
curr2 = p.Next;
//make 2 links have the same distance to the intersect
if (M > N)
{
for (int i = 0; i < M - N; i++)
curr1 = curr1.Next;
}
else if (M < N)
{
for (int i = 0; i < N - M; i++)
curr2 = curr2.Next;
}
//goto the intersect
while (curr1 != p)
{
if (curr1 == curr2)
{
return curr1;
}
curr1 = curr1.Next;
curr2 = curr2.Next;
}
return null;
}
这道题有多种算法。
算法1:把第一个链表逐项存在hashtable中,遍历第2个链表的每一项,如果能在第一个链表中找到,则必然相交。
static bool JudgeIntersectLink1(Link head1,Link head2)
{
Hashtable ht = new Hashtable();
Link curr1 = head1;
Link curr2 = head2;
//store all the elements of link1
while (curr1.Next != null)
{
ht[curr1.Next] = string.Empty;
curr1 = curr1.Next;
}
//check all the elements in link2 if exists in Hashtable or not
while (curr2.Next != null)
{
//if exists
if (ht[curr2.Next] != null)
{
return true;
}
curr2 = curr2.Next;
}
return false;
}
算法2:把一个链表A接在另一个链表B的末尾,如果有环,则必然相交。如何判断有环呢?从A开始遍历,如果能回到A的表头,则肯定有环。
注意,在返回结果之前,要把刚才连接上的两个链表断开,恢复原状。
static bool JudgeIntersectLink2(Link head1,Link head2)
{
bool exists = false;
Link curr1 = head1;
Link curr2 = head2;
//goto the end of the link1
while (curr1.Next != null)
{
curr1 = curr1.Next;
}
//join these two links
curr1.Next = head2;
//iterate link2
while(curr2.Next != null)
{
if (curr2.Next == head2)
{
exists = true;
break;
}
curr2 = curr2.Next;
}
//recover original status, whether exists or not
curr1.Next = null;
return exists;
}
算法3:如果两个链表的末尾元素相同,则必相交。
static bool JudgeIntersectLink3(Link head1,Link head2)
{
Link curr1 = head1;
Link curr2 = head2;
//goto the end of the link1
while (curr1.Next != null)
{
curr1 = curr1.Next;
}
//goto the end of the link2
while (curr2.Next != null)
{
curr2 = curr2.Next;
}
if (curr1 != curr2)
return false;
else
return true;
}
分别遍历两个单链表,计算出它们的长度M和N,假设M比N大,则长度M的链表先前进M-N,然后两个链表同时以步长1前进,前进的同时比较当前的元素,如果相同,则必是交点。
public static Link GetIntersect(Link head1,Link head2)
{
Link curr1 = head1;
Link curr2 = head2;
int M = 0, N = 0;
//goto the end of the link1
while (curr1.Next != null)
{
curr1 = curr1.Next;
M++;
}
//goto the end of the link2
while (curr2.Next != null)
{
curr2 = curr2.Next;
N++;
}
//return to the begining of the link
curr1 = head1;
curr2 = head2;
if (M > N)
{
for (int i = 0; i < M - N; i++)
curr1 = curr1.Next;
}
else if (M < N)
{
for (int i = 0; i < N - M; i++)
curr2 = curr2.Next;
}
while (curr1.Next != null)
{
if (curr1 == curr2)
{
return curr1;
}
curr1 = curr1.Next;
curr2 = curr2.Next;
}
return null;
}
例如:9>9>9>NULL+ 1>NULL =>
1>0>0>0>NULL
肯定是使用递归啦,不然没办法解决进位+1问题,因为这时候要让前面的节点加1,而我们的单链表是永远指向前的。
此外对于999+1=1000,新得到的值的位数(4位)比原来的两个值(1个1位,1个3位)都多,所以我们将表头的值设置为0,如果多出一位来,就暂时存放到表头。递归结束后,如果表头为1,就在新的链表外再加一个新的表头。
//head1 length > head2, so M > N
public static int Add(Link head1, Linkhead2, ref Link newHead, int M, int N)
{
// goto the end
if (head1 == null)
return 0;
int temp = 0;
int result = 0;
newHead = new Link(null, 0);
if (M > N)
{
result = Add(head1.Next, head2, ref newHead.Next, M - 1, N);
temp = head1.Data + result;
newHead.Data = temp % 10;
return temp >= 10
1 :0;
}
else // M == N
{
result = Add(head1.Next, head2.Next, ref newHead.Next, M - 1, N - 1);
temp = head1.Data + head2.Data + +result;
newHead.Data = temp % 10;
return temp >= 10
1 :0;
}
}
这里假设head1比head2长,而且M、N分别是head1和head2的长度。
无外乎是冒泡、选择、插入等排序方法。关键是交换算法,需要额外考虑。第7题我编写了一个交换算法,在本题的排序过程中,我们可以在外层和内层循环里面,捕捉到pre1和pre2,然后进行交换,而无需每次交换又要遍历一次单链表。
在实践中,我发现冒泡排序和选择排序都要求内层循环从链表的末尾向前走,这明显是不合时宜的。
所以我最终选择了插入排序算法,如下所示:
先给出基于数组的算法:
代码
static int[]
InsertSort(int[] arr)
{
for(int i=1; i<arr.Length;i++)
{
for(int j =i; (j>0)&&arr[j]<arr[j-1];j--)
{
arr[j]=arr[j]^arr[j-1];
arr[j-1]=arr[j]^arr[j-1];
arr[j]=arr[j]^arr[j-1];
}
}
return arr;
}
仿照上面的思想,我们来编写基于Link的算法:
public static Link SortLink(Link head)
{
Link pre1 = head;
Link pre2 = head.Next;
Link min = null;
for (Link curr1 = head.Next; curr1 != null; curr1 = min.Next)
{
if (curr1.Next == null)
break;
min = curr1;
for (Link curr2 = curr1.Next; curr2 != null; curr2 = curr2.Next)
{
//swap curr1 and curr2
if (curr2.Data < curr1.Data)
{
min = curr2;
curr2 = curr1;
curr1 = min;
pre1.Next = curr1;
curr2.Next = curr1.Next;
curr1.Next = pre2;
//if exchange element n-1 andn, no need to add reference from pre2 to curr2, because they are the same one
if (pre2 != curr2)
pre2.Next = curr2;
}
pre2 = curr2;
}
pre1 = min;
pre2 = min.Next;
}
return head;
}
值得注意的是,很多人的算法不能交换相邻两个元素,这是因为pre2和curr2是相等的,如果此时还执行pre2.Next = curr2; 会造成一个自己引用自己的环。
交换指针很是麻烦,而且效率也不高,需要经常排序的东西最好不要用链表来实现,还是数组好一些。
用Hashtable辅助,遍历一遍单链表就能搞定。
实践中发现,curr从表头开始,每次判断下一个元素curr.Netx是否重复,如果重复直接使用curr.Next =curr.Next.Next; 就可以删除重复元素——这是最好的算法。唯一的例外就是表尾,所以到达表尾,就break跳出while循环。
public static Link DeleteDuplexElements(Linkhead)
{
Hashtable ht = new Hashtable();
Link curr = head;
while (curr != null)
{
if (curr.Next == null)
{
break;
}
if (ht[curr.Next.Data] != null)
{
curr.Next = curr.Next.Next;
}
else
{
ht[curr.Next.Data] = "";
}
curr = curr.Next;
}
return head;
}
结语:
单链表只有一个向前指针Next,所以要使用1-2个额外变量来存储当前元素的前一个或后一个指针。
尽量用while循环而不要用for循环,来进行遍历。
哇塞,我就是不用指针,照样能“修改地址”,达到和C++同样的效果,虽然很烦~
遍历的时候,不要在while循环中head=head.Next;这样会改变原先的数据结构。我们要这么写:Link curr=head;然后curr=curr.Next;
有时我们需要临时把环切开,有时我们需要临时把单链表首尾相连成一个环。
究竟是玩curr还是curr.Next,根据不同题目而各有用武之地,没有定论,不必强求。
目录:
1.设计含min函数的栈,要求min、push和pop的时间复杂度都是o(1)。
2.设计含min函数的栈的另解
3.用两个栈实现队列
4.用两个队列实现栈
5.栈的push、pop序列是否一致
6.递归反转一个栈,要求不得重新申请一个同样的栈,空间复杂度o(1)
7.给栈排个序
8..如何用一个数组实现两个栈
9..如何用一个数组实现三个栈
算法思想:需要设计一个辅助栈,用来存储当前栈中元素的最小值。网上有人说存储当前栈中元素的最小值的所在位置,虽然能节省空间,这其实是不对的,因为我在调用Min函数的时候,只能得到位置,还要对存储元素的栈不断的pop,才能得到最小值——时间复杂度o(1)。
所以,还是在辅助栈中存储元素吧。
此外,还要额外注意Push操作,第一个元素不用比较,自动成为最小值入栈。其它元素每次都要和栈顶元素比较,小的那个放到栈顶。
public class NewStack
{
private Stack dataStack;
private Stack mindataStack;
public NewStack()
{
dataStack = new Stack();
mindataStack = new Stack();
}
public void Push(int element)
{
dataStack.Push(element);
if (mindataStack.Count == 0)
mindataStack.Push(element);
else if (element <= (int)mindataStack.Peek())
mindataStack.Push(element);
else //(element > mindataStack.Peek)
mindataStack.Push(mindataStack.Peek());
}
public int Pop()
{
if (dataStack.Count == 0)
throw new Exception("The stack is empty");
mindataStack.Pop();
return (int)dataStack.Pop();
}
public int Min()
{
if (dataStack.Count == 0)
throw new Exception("The stack is empty");
return (int)mindataStack.Peek();
}
}
话说,和青菜脸呆久了,就沾染了上海小市民意识,再加上原本我就很抠门儿,于是对于上一题目,我把一个栈当成两个用,就是说,每次push,先入站当前元素,然后入栈当前栈中最小元素;pop则每次弹出2个元素。
算法代码如下所示(这里最小元素位于当前元素之上,为了下次比较方便):
public class NewStack
{
private Stack stack;
public NewStack()
{
stack = new Stack();
}
public void Push(int element)
{
if (stack.Count == 0)
{
stack.Push(element);
stack.Push(element);
}
else if (element <= (int)stack.Peek())
{
stack.Push(element);
stack.Push(element);
}
else //(element > stack.Peek)
{
object min = stack.Peek();
stack.Push(element);
stack.Push(min);
}
}
public int Pop()
{
if (stack.Count == 0)
throw new Exception("The stack is empty");
stack.Pop();
return (int)stack.Pop();
}
public int Min()
{
if (stack.Count == 0)
throw new Exception("The stack is empty");
return (int)stack.Peek();
}
}
之所以说我这个算法比较叩门,是因为我只使用了一个栈,空间复杂度o(N),节省了一半的空间(算法1的空间复杂度o(2N))。
实现队列,就要实现它的3个方法:Enqueue(入队)、Dequeue(出队)和Peek(队头)。
1)stack1存的是每次进来的元素,所以Enqueue就是把进来的元素push到stack1中。
2)而对于Dequeue,一开始stack2是空的,所以我们把stack1中的元素全都pop到stack2中,这样stack2的栈顶就是队头。只要stack2不为空,那么每次出队,就相当于stack2的pop。
3)接下来,每入队一个元素,仍然push到stack1中。每出队一个元素,如果stack2不为空,就从stack2中pop一个元素;如果stack2为空,就重复上面的操作——把stack1中的元素全都pop到stack2中。
4)Peek操作,类似于Dequeue,只是不需要出队,所以我们调用stack2的Peek操作。当然,如果stack2为空,就把stack1中的元素全都pop到stack2中。
5)注意边界的处理,如果stack2和stack1都为空,才等于队列为空,此时不能进行Peek和Dequeue操作。
按照上述分析,算法实现如下:
public class NewQueue
{
private Stack stack1;
private Stack stack2;
public NewQueue()
{
stack1 = new Stack();
stack2 = new Stack();
}
public void Enqueue(int element)
{
stack1.Push(element);
}
public int Dequeue()
{
if (stack2.Count == 0)
{
if (stack1.Count == 0)
throw new Exception("Thequeue is empty");
else
while (stack1.Count > 0)
stack2.Push(stack1.Pop());
}
return (int)stack2.Pop();
}
public int Peek()
{
if (stack2.Count == 0)
{
if (stack1.Count == 0)
throw new Exception("Thequeue is empty");
else
while (stack1.Count > 0)
stack2.Push(stack1.Pop());
}
return (int)stack2.Peek();
}
}
这个嘛,就要queue1和queue2轮流存储数据了。这个“轮流”发生在Pop和Peek的时候,假设此时我们把所有数据存在queue1中(此时queue2为空),我们把queue1的n-1个元素放到queue2中,queue中最后一个元素就是我们想要pop的元素,此时queue2存有n-1个元素(queue1为空)。
至于Peek,则是每次转移n个数据,再转移最后一个元素的时候,将其计下并返回。
那么Push的操作,则需要判断当前queue1和queue2哪个为空,将新元素放到不为空的队列中。
public class NewStack
{
private Queue queue1;
private Queue queue2;
public NewStack()
{
queue1 = new Queue();
queue2 = new Queue();
}
public void Push(int element)
{
if (queue1.Count == 0)
queue2.Enqueue(element);
else
queue1.Enqueue(element);
}
public int Pop()
{
if (queue1.Count == 0 && queue2.Count == 0)
throw new Exception("The stack is empty");
if (queue1.Count > 0)
{
while (queue1.Count > 1)
{
queue2.Enqueue(queue1.Dequeue());
}
//还剩一个
return (int)queue1.Dequeue();
}
else //queue2.Count > 0
{
while (queue2.Count > 1)
{
queue1.Enqueue(queue2.Dequeue());
}
//还剩一个
return (int)queue2.Dequeue();
}
}
public int Peek()
{
if (queue1.Count == 0 && queue2.Count == 0)
throw new Exception("The stack is empty");
int result = 0;
if (queue1.Count > 0)
{
while (queue1.Count > 1)
{
queue2.Enqueue(queue1.Dequeue());
}
//还剩一个
result = (int)queue1.Dequeue();
queue2.Enqueue(result);
}
else //queue2.Count > 0
{
while (queue2.Count > 1)
{
queue1.Enqueue(queue2.Dequeue());
}
//还剩一个
result = (int)queue2.Dequeue();
queue1.Enqueue(result);
}
return result;
}
}
输入两个整数序列。其中一个序列表示栈的push顺序,判断另一个序列有没有可能是对应的pop顺序。为了简单起见,我们假设push序列的任意两个整数都是不相等的。
比如输入的push序列是1、2、3、4、5,那么4、5、3、2、1就有可能是一个pop系列。因为可以有如下的push和pop序列:push 1,push 2,push 3,push 4,pop,push 5,pop,pop,pop,pop,这样得到的pop序列就是4、5、3、2、1。但序列4、3、5、1、2就不可能是push序列1、2、3、4、5的pop序列。
网上的若干算法都太复杂了,现提出包氏算法如下:
先for循环把arr1中的元素入栈,并在每次遍历时,检索arr2中可以pop的元素。如果循环结束,而stack中还有元素,就说明arr2序列不是pop序列。
static bool
JudgeSequenceIsPossible(int[] arr1,int[] arr2)
{
Stack stack = newStack();
for (int i = 0,j = 0; i < arr1.Length; i++)
{
stack.Push(arr1[i]);
while(stack.Count > 0 && (int)stack.Peek() == arr2[j])
{
stack.Pop();
j++;
}
}
return stack.Count== 0;
}
算法思想:汉诺塔的思想,非常复杂,玩过九连环的人都想得通的
static void ReverseStack(ref Stack stack)
{
if (stack.Count == 0)
return;
object top = stack.Pop();
ReverseStack(ref stack);
if (stack.Count == 0)
{
stack.Push(top);
return;
}
object top2 = stack.Pop();
ReverseStack(ref stack);
stack.Push(top);
ReverseStack(ref stack);
stack.Push(top2);
}
本题目是上一题目的延伸
static void Sort(ref Stack stack)
{
if (stack.Count == 0)
return;
object top = stack.Pop();
Sort(ref stack);
if (stack.Count == 0)
{
stack.Push(top);
return;
}
object top2 = stack.Pop();
if ((int)top > (int)top2)
{
stack.Push(top);
Sort(ref stack);
stack.Push(top2);
}
else
{
stack.Push(top2);
Sort(ref stack);
stack.Push(top);
}
}
继续我所提倡的抠门儿思想,也不枉我和青菜脸相交一场。
网上流传着两种方法:
方法1
采用交叉索引的方法
一号栈所占数组索引为0, 2, 4, 6, 8......(K*2)
二号栈所占数组索引为1,3,5,7,9 ......(K*2 + 1)
算法实现如下:
public class NewStack
{
object[] arr;
int top1;
int top2;
public NewStack(int capticy)
{
arr = new object[capticy];
top1 = -1;
top2 = -2;
}
public void Push(int type, object element)
{
if (type == 1)
{
if (top1 + 2 >= arr.Length)
throw new Exception("Thestack is full");
else
{
top1 += 2;
arr[top1] = element;
}
}
else //type==2
{
if (top2 + 2 >= arr.Length)
throw new Exception("Thestack is full");
else
{
top2 += 2;
arr[top2] = element;
}
}
}
public object Pop(int type)
{
object obj = null;
if (type == 1)
{
if (top1 == -1)
throw new Exception("Thestack is empty");
else
{
obj = arr[top1];
arr[top1] = null;
top1 -= 2;
}
}
else //type == 2
{
if (top2 == -2)
throw new Exception("Thestack is empty");
else
{
obj = arr[top2];
arr[top2] = null;
top2 -= 2;
}
}
return obj;
}
public object Peek(int type)
{
if (type == 1)
{
if (top1 == -1)
throw new Exception("Thestack is empty");
return arr[top1];
}
else //type == 2
{
if (top2 == -2)
throw new Exception("Thestack is empty");
return arr[top2];
}
}
}
方法2:
第一个栈A:从最左向右增长
第二个栈B:从最右向左增长
代码实现如下:
public class NewStack
{
object[] arr;
int top1;
int top2;
public NewStack(int capticy)
{
arr = new object[capticy];
top1 = 0;
top2 = capticy;
}
public void Push(int type, object element)
{
if (top1 == top2)
throw new Exception("The stack is full");
if (type == 1)
{
arr[top1] = element;
top1++;
}
else //type==2
{
top2--;
arr[top2] = element;
}
}
public object Pop(int type)
{
object obj = null;
if (type == 1)
{
if (top1 == 0)
throw new Exception("Thestack is empty");
else
{
top1--;
obj = arr[top1];
arr[top1] = null;
}
}
else //type == 2
{
if (top2 == arr.Length)
throw new Exception("Thestack is empty");
else
{
obj = arr[top2];
arr[top2] = null;
top2++;
}
}
return obj;
}
public object Peek(int type)
{
if (type == 1)
{
if (top1 == 0)
throw new Exception("Thestack is empty");
return arr[top1 - 1];
}
else //type == 2
{
if (top2 == arr.Length)
throw new Exception("Thestack is empty");
return arr[top2];
}
}
}
综合比较上述两种算法,我们发现,算法1实现的两个栈,每个都只有n/2个空间大小;而算法2实现的两个栈,如果其中一个很小,另一个则可以很大,它们的和为常数n。
最后,让我们把抠门儿进行到底,相信看完本文,你已经从物质和精神上都升级为一个抠门儿主义者。
如果还使用交叉索引的办法,每个栈都只有N/3个空间。
让我们只好使用上个题目的第2个方法,不过这只能容纳2个栈,我们还需要一个位置存放第3个栈,不如考虑数组中间的位置——第3个栈的增长规律可以如下:
第1个入栈C的元素进mid处
第2个入栈C的元素进mid+1处
第3个入栈C的元素进mid-1处
第4个入栈C的元素进mid+2处
这个方法的好处是, 每个栈都有接近N/3个空间。
public class NewStack
{
object[] arr;
int top1;
int top2;
int top3_left;
int top3_right;
bool isLeft;
public NewStack(int capticy)
{
arr = new object[capticy];
top1 = 0;
top2 = capticy;
isLeft = true;
top3_left = capticy / 2;
top3_right = top3_left + 1;
}
public void Push(int type, object element)
{
if (type == 1)
{
if (top1 == top3_left + 1)
throw new Exception("Thestack is full");
arr[top1] = element;
top1++;
}
else if (type == 2)
{
if (top2 == top3_right)
throw new Exception("Thestack is full");
top2--;
arr[top2] = element;
}
else //type==3
{
if (isLeft)
{
if (top1 == top3_left + 1)
throw newException("The stack is full");
arr[top3_left] = element;
top3_left--;
}
else
{
if (top2 == top3_right)
throw newException("The stack is full");
arr[top3_right] = element;
top3_right++;
}
isLeft = !isLeft;
}
}
public object Pop(int type)
{
object obj = null;
if (type == 1)
{
if (top1 == 0)
throw new Exception("Thestack is empty");
else
{
top1--;
obj = arr[top1];
arr[top1] = null;
}
}
else if (type == 2)
{
if (top2 == arr.Length)
throw new Exception("Thestack is empty");
else
{
obj = arr[top2];
arr[top2] = null;
top2++;
}
}
else //type==3
{
if (top3_right == top3_left + 1)
throw new Exception("Thestack is empty");
if (isLeft)
{
top3_left++;
obj = arr[top3_left];
arr[top3_left] = null;
}
else
{
top3_right--;
obj = arr[top3_right];
arr[top3_right] = null;
}
isLeft = !isLeft;
}
return obj;
}
public object Peek(int type)
{
if (type == 1)
{
if (top1 == 0)
throw new Exception("Thestack is empty");
return arr[top1 - 1];
}
else if (type == 2)
{
if (top2 == arr.Length)
throw newException("The stack is empty");
return arr[top2];
}
else //type==3
{
if (top3_right == top3_left + 1)
throw new Exception("Thestack is empty");
if (isLeft)
return arr[top3_left + 1];
else
return arr[top3_right - 1];
}
}
}
目录:
1.二叉树三种周游(traversal)方式:
2.怎样从顶部开始逐层打印二叉树结点数据
3.如何判断一棵二叉树是否是平衡二叉树
4.设计一个算法,找出二叉树上任意两个节点的最近共同父结点,复杂度如果是O(n2)则不得分。
5.如何不用递归实现二叉树的前序/后序/中序遍历?
6.在二叉树中找出和为某一值的所有路径
7.怎样编写一个程序,把一个有序整数数组放到二叉树中?
8.判断整数序列是不是二叉搜索树的后序遍历结果
9.求二叉树的镜像
10.一棵排序二叉树(即二叉搜索树BST),令 f=(最大值+最小值)/2,设计一个算法,找出距离f值最近、大于f值的结点。复杂度如果是O(n2)则不得分。
11.把二叉搜索树转变成排序的双向链表
首先写一个二叉树的C#实现,这是我们的基石:
public class BinNode
{
public int Element;
public BinNode Left;
public BinNode Right;
public BinNode(int element, BinNode left, BinNode right)
{
this.Element = element;
this.Left = left;
this.Right = right;
}
public bool IsLeaf()
{
return this.Left == null && this.Right == null;
}
}
1)前序周游(preorder):节点 –> 子节点Left(包括其子树) –> 子节点Right(包括其子树)
static void PreOrder(BinNode root)
{
if (root == null)
return;
//visit current node
Console.WriteLine(root.Element);
PreOrder(root.Left);
PreOrder(root.Right);
}
2)后序周游(postorder):子节点Left(包括其子树) –> 子节点Right(包括其子树) –> 节点
static void PostOrder(BinNode root)
{
if (root == null)
return;
PostOrder(root.Left);
PostOrder(root.Right);
//visit current node
Console.WriteLine(root.Element);
}
3)中序周游(inorder):子节点Left(包括其子树) –> 节点 –> 子节点Right(包括其子树)
static void InOrder(BinNode root)
{
if (root == null)
return;
InOrder(root.Left);
//visit current node
Console.WriteLine(root.Element);
InOrder(root.Right);
}
我们发现,三种周游的code实现,仅仅是访问当前节点的这条语句所在位置不同而已。
有2种算法:
算法1:基于Queue来实现,也就是广度优先搜索(BFS)的思想
static void PrintTree1(BinNode root)
{
if (root == null) return;
BinNode tmp = null;
Queue queue = new Queue();
queue.Enqueue(root);
while (queue.Count > 0)
{
tmp = (BinNode)queue.Dequeue();
Console.WriteLine(tmp.Element);
if (tmp.Left != null)
queue.Enqueue(tmp.Left);
if (tmp.Right != null)
queue.Enqueue(tmp.Right);
}
}
话说,BFS和DFS思想本来是用于图的,但我们不能被传统的思维方式所束缚。
算法2:基于单链表实现
如果没有Queue给我们用,我们只好使用单链表,把每个节点存在单链表的Data中,实现如下:
public class Link
{
public Link Next;
public BinNode Data;
public Link(Link next, BinNode data)
{
this.Next = next;
this.Data = data;
}
}
看过了Queue的实现,我们发现永远是先出队1个(队头),然后入队2个(把出队的Left和Right放到队尾)。
对于单链表而言,我们可以先模拟入队——把first的Data所对应的Left和Right,先后插到second的后面,即second.Next和second.Next.Next位置,同时second向前走0、1或2次,再次到达链表末尾,这取决于Left和Right是否为空;然后我们模拟出队——first前进1步。
当first指针走不下去了,那么任务也就结束了。
static void PrintTree2(BinNode root)
{
if (root == null) return;
Link head = new Link(null, root);
Link first = head;
Link second = head;
while (first != null)
{
if (first.Data.Left != null)
{
second.Next = new Link(null, first.Data.Left);
second = second.Next;
}
if (first.Data.Right != null)
{
second.Next = new Link(null, first.Data.Right);
second = second.Next;
}
Console.WriteLine(first.Data.Element);
first = first.Next;
}
}
平衡二叉树的定义,如果任意节点的左右子树的深度相差不超过1,那这棵树就是平衡二叉树。
算法思路:先编写一个计算二叉树深度的函数GetDepth,利用递归实现;然后再递归判断每个节点的左右子树的深度是否相差1
static int GetDepth(BinNode root)
{
if (root == null)
return 0;
int leftLength = GetDepth(root.Left);
int rightLength = GetDepth(root.Right);
return (leftLength > rightLength
leftLength : rightLength) + 1;
}
注意这里的+1,对应于root不为空(算作当前1个深度)
static bool IsBalanceTree(BinNode root)
{
if (root == null)
return true;
int leftLength = GetDepth(root.Left);
int rightLength = GetDepth(root.Right);
int distance = leftLength > rightLength
leftLength - rightLength : rightLength -leftLength;
if (distance > 1)
return false;
else
return IsBalanceTree(root.Left) && IsBalanceTree(root.Right);
}
上述程序的逻辑是,只要当前节点root的Left和Right深度差不超过1,就递归判断Left和Right是否也符合条件,直到为Left或Right为null,这意味着它们的深度为0,能走到这一步,前面必然都符合条件,所以整个二叉树都符合条件。
本题网上有很多算法,都不怎么样。这里提出包氏的两个算法:
算法1:做一个容器,我们在遍历二叉树寻找节点的同时,把从根到节点的路径扔进去(两个节点就是两个容器)。由于根节点最后一个被扔进去,但我们接下来又需要第一个就能访问到它——后进先出,所以这个容器是一个栈。时间复杂度O(N),空间复杂度O(N)。
static bool GetPositionByNode(BinNode root,BinNode node, ref Stack stack)
{
if (root == null)
return false;
if (root == node)
{
stack.Push(root);
return true;
}
if (GetPositionByNode(root.Left, node, ref stack) ||GetPositionByNode(root.Right, node, ref stack))
{
stack.Push(root);
return true;
}
return false;
}
然后我们要同时弹出这两个容器的元素,直到它们不相等,那么之前那个相等的元素就是我们要求的父亲节点。
static BinNode FindParentNode(BinNode root,BinNode node1, BinNode node2)
{
Stack stack1 = new Stack();
GetPositionByNode(root, node1, ref stack1);
Stack stack2 = new Stack();
GetPositionByNode(root, node2, ref stack2);
BinNode tempNode = null;
while (stack1.Peek() == stack2.Peek())
{
tempNode = (BinNode)stack1.Pop();
stack2.Pop();
}
return tempNode;
}
算法2:如果要求o(1)的空间复杂度,就是说,只能用一个变量来辅助我们。
我们选择一个64位的整数,然后从1开始,从左到右逐层为二叉树的每个元素赋值,root对应1,root.Left对应2,root.Right对应3,依次类推,而不管实际这个位置上是否有节点,我们发现两个规律:
//// 1
//// 2 3
//// 4 5 6 7
//// 8 9 10
如果要找的是5和9位置上的节点。
我们发现,它们的二进制分别是101和1001,右移1001使之与101位数相同,于是1001变成了100(也就是9的父亲4)。
这时101和100(也就是4和5位于同样的深度),我们从左往右找,101和100具有2位相同,即10,这就是我们要找的4和5的父亲,也就是9和5的最近父亲。
由上面观察,得到算法:
1)将找到的两个节点对应的数字
static bool GetPositionByNode(BinNode root,BinNode node, ref int pos)
{
if (root == null)
return false;
if (root == node)
return true;
int temp = pos;
//这么写很别扭,但是能保证只要找到就不再进行下去
pos = temp * 2;
if (GetPositionByNode(root.Left, node, ref pos))
{
return true;
}
else
{
//找不到左边找右边
pos = temp * 2 + 1;
return GetPositionByNode(root.Right, node, ref pos);
}
}
2)它们的二进制表示,从左向右逐一比较,直到一个结束或不再相同,则最大的相同子串,就是我们需要得到的最近父亲所对应的位置K。
static int FindParentPosition(int larger,int smaller)
{
if (larger == smaller) return larger;
int left = GetLen(larger) - GetLen(smaller);
while (left > 0)
{
larger = larger >> 1;
left--;
}
while (larger != smaller)
{
larger = larger >> 1;
smaller = smaller >> 1;
}
return smaller;
}
static int GetLen(int num)
{
int length = 0;
while (num != 0)
{
num = num >> 1;
length++;
}
return length;
}
3)第3次递归遍历,寻找K所对应的节点。
函数GetNodeByPosition的思想是,先算出k在第几层power,观察k的二进制表示,比如说12,即1100,从左向右数第一个位1不算,还剩下100,1表示向右走,0表示向左走,于是从root出发,1->3->6->12。
static BinNode GetNodeByPosition(BinNoderoot, int num)
{
if (num == 1) return root;
int pow = (int)Math.Floor(Math.Log(num, 2)); //1 return 0, 2-3 return 1,4-7 return 2
//第一个位不算
num -= 1 << pow;
while (pow > 0)
{
if ((num & 1 << (pow - 1)) == 0)
root = root.Left;
else
root = root.Right;
pow--;
}
return root;
}
总结上面的3个步骤:
static BinNode FindParentNode(BinNode root,BinNode node1, BinNode node2)
{
int pos1 = 1;
GetPositionByNode(root, node1, ref pos1);
int pos2 = 1;
GetPositionByNode(root, node2, ref pos2);
int parentposition = 0;
if (pos1 >= pos2)
{
parentposition = FindParentPosition(pos1, pos2);
}
else //pos1<pos2
{
parentposition = FindParentPosition(pos2, pos1);
}
return GetNodeByPosition(root, parentposition);
}
算法思想:三种算法的思想都是让root的Left的Left的Left全都入栈。所以第一个while循环的逻辑,都是相同的。
下面详细分析第2个while循环,这是一个出栈动作,只要栈不为空,就始终要弹出栈顶元素,由于我们之前入栈的都是Left节点,所以每次在出栈的时候,我们都要考虑Right节点是否存在。因为前序/后序/中序遍历顺序的不同,所以在具体的实现上有略为区别。
1)前序遍历
这个是最简单的。
前序遍历是root->root.Left->root.Right的顺序。
因为在第一个while循环中,每次进栈的都可以认为是一个root,所以我们直接打印,然后root.Right和root.Left先后进栈,那么出栈的时候,就能确保先左后右的顺序。
static void PreOrder(BinNode root)
{
Stack stack = new Stack();
BinNode temp = root;
//入栈
while (temp != null)
{
Console.WriteLine(temp.Element);
if (temp.Right != null)
stack.Push(temp.Right);
temp = temp.Left;
}
//出栈,当然也有入栈
while (stack.Count > 0)
{
temp = (BinNode)stack.Pop();
Console.WriteLine(temp.Element);
while (temp != null)
{
if (temp.Right != null)
stack.Push(temp.Right);
temp = temp.Left;
}
}
}
//后序遍历比较麻烦,需要记录上一个访问的节点,然后在本次循环中判断当前节点的Right或Left是否为上个节点,当前节点的Right为null表示没有右节点。
static void PostOrder(BinNode root)
{
Stack stack = new Stack();
BinNode temp = root;
//入栈
while (temp != null)
{
if (temp != null)
stack.Push(temp);
temp = temp.Left;
}
//出栈,当然也有入栈
while (stack.Count > 0)
{
BinNode lastvisit = temp;
temp = (BinNode)stack.Pop();
if (temp.Right == null || temp.Right == lastvisit)
{
Console.WriteLine(temp.Element);
}
else if (temp.Left == lastvisit)
{
stack.Push(temp);
temp = temp.Right;
stack.Push(temp);
while (temp != null)
{
if (temp.Left != null)
stack.Push(temp.Left);
temp = temp.Left;
}
}
}
}
//中序遍历,类似于前序遍历
static void InOrder(BinNode root)
{
Stack stack = new Stack();
BinNode temp = root;
//入栈
while (temp != null)
{
if (temp != null)
stack.Push(temp);
temp = temp.Left;
}
//出栈,当然也有入栈
while(stack.Count > 0)
{
temp = (BinNode)stack.Pop();
Console.WriteLine(temp.Element);
if (temp.Right != null)
{
temp = temp.Right;
stack.Push(temp);
while (temp != null)
{
if (temp.Left != null)
stack.Push(temp.Left);
temp = temp.Left;
}
}
}
}
算法思想:这道题目的苦恼在于,如果用递归,只能打出一条路径来,其它符合条件的路径打不出来。
为此,我们需要一个Stack,来保存访问过的节点,即在对该节点的递归前让其进栈,对该节点的递归结束后,再让其出栈——深度优先原则(DFS)。
此外,在递归中,如果发现某节点(及其路径)符合条件,如何从头到尾打印是比较头疼的,因为DFS使用的是stack而不是queue,为此我们需要一个临时栈,来辅助打印。
static void FindBinNode(BinNode root, intsum, Stack stack)
{
if (root == null)
return;
stack.Push(root.Element);
//Leaf
if (root.IsLeaf())
{
if (root.Element == sum)
{
Stack tempStack = new Stack();
while (stack.Count > 0)
{
tempStack.Push(stack.Pop());
}
while (tempStack.Count > 0)
{
Console.WriteLine(tempStack.Peek());
stack.Push(tempStack.Pop());
}
Console.WriteLine();
}
}
if (root.Left != null)
FindBinNode(root.Left, sum - root.Element, stack);
if (root.Right != null)
FindBinNode(root.Right, sum - root.Element, stack);
stack.Pop();
}
算法思想:我们该如何构造这棵二叉树呢?当然是越平衡越好,如下所示:
//// arr[0]
//// arr[1] arr[2]
//// arr[3] arr[4] arr[5]
相应编码如下:
public static voidInsertArrayIntoTree(int[] arr, int pos, ref BinNode root)
{
root = new BinNode(arr[pos], null, null);
root.Element = arr[pos];
//if Left value less than arr length
if (pos * 2 + 1 > arr.Length - 1)
{
return;
}
else
{
InsertArrayIntoTree(arr, pos * 2 + 1, ref root.Left);
}
//if Right value less than arr length
if (pos * 2 + 2 > arr.Length - 1)
{
return;
}
else
{
root.Right = new BinNode(arr[pos * 2 + 2], null, null);
InsertArrayIntoTree(arr, pos * 2 + 2, ref root.Right);
}
}
比如,给你一个数组: inta[] = [1, 6, 4, 3, 5] ,则F(a) => false
算法思想:在后续遍历得到的序列中,最后一个元素为树的根结点。从头开始扫描这个序列,比根结点小的元素都应该位于序列的左半部分;从第一个大于跟结点开始到跟结点前面的一个元素为止,所有元素都应该大于跟结点,因为这部分元素对应的是树的右子树。根据这样的划分,把序列划分为左右两部分,我们递归地确认序列的左、右两部分是不是都是二元查找树。
由于不能使用动态数组,所以我们每次递归都使用同一个数组arr,通过start和length来模拟“部分”数组。
public static bool VerifyArrayOfBST(int[]arr, int start, int length)
{
if (arr == null || arr.Length == 0 || arr.Length == 1)
{
return false;
}
int root = arr[length + start - 1];
int i = start;
for (; i < length - 1; i++)
{
if (arr[i] >= root)
break;
}
int j = i;
for (; j < length - 1; j++)
{
if (arr[j] < root)
return false;
}
bool left = true;
if (i > start)
{
left = VerifyArrayOfBST(arr, start, i - start);
}
bool right = true;
if (j > i)
{
right = VerifyArrayOfBST(arr, i, j - i + 1);
}
return left && right;
}
算法1:利用上述遍历二叉树的方法(比如说前序遍历),把访问操作修改为交换左右节点的逻辑:
static void PreOrder(ref BinNode root)
{
if (root == null)
return;
//visit current node
BinNode temp = root.Left;
root.Left = root.Right;
root.Right = temp;
PreOrder(ref root.Left);
PreOrder(ref root.Right);
}
算法2:使用循环也可以完成相同的功能。
static void PreOrder2(ref BinNode root)
{
if (root == null)
return;
Stack stack = new Stack();
stack.Push(root);
while (stack.Count > 0)
{
//visit current node
BinNode temp = root.Left;
root.Left = root.Right;
root.Right = temp;
if (root.Left != null)
stack.Push(root.Left);
if (root.Right != null)
stack.Push(root.Right);
}
}
算法思想:最小最大节点分别在最左下与最右下节点,O(N)
static BinNode Find(BinNode root)
{
BinNode min = FindMinNode(root);
BinNode max = FindMaxNode(root);
double find = (double)(min.Element + max.Element) / 2;
return FindNode(root, find);
}
static BinNode FindMinNode(BinNode root)
{
BinNode min = root;
while (min.Left != null)
{
min = min.Left;
}
return min;
}
static BinNode FindMaxNode(BinNode root)
{
BinNode max = root;
while (max.Right != null)
{
max = max.Right;
}
return max;
}
递归寻找BST的节点,O(logN)。
static BinNode FindNode(BinNode root,double mid)
{
//如果小于相等,则从右边找一个最小值
if (root.Element <= mid)
{
if (root.Right == null)
return root;
BinNode find = FindNode(root.Right, mid);
//不一定找得到
return find.Element < mid
root: find;
}
//如果大于,则找到Left
else //temp.Element > find
{
if (root.Left == null)
return root;
BinNode find = FindNode(root.Left, mid);
//不一定找得到
return find.Element < mid
root: find;
}
}
//// 13
//// 10 15
//// 5 11 17
//// 16 22
转变为Link:5=10=11=13=15=16=17=22
算法思想:这个就是中序遍历啦,因为BST的中序遍历就是一个从小到大的访问顺序。局部修改中序遍历算法,于是有如下代码:
static void ConvertNodeToLink(BinNode root,ref DoubleLink link)
{
if (root == null)
return;
BinNode temp = root;
if (temp.Left != null)
ConvertNodeToLink(temp.Left, ref link);
//visit current node
link.Next = new DoubleLink(link, null, root);
link = link.Next;
if (temp.Right != null)
ConvertNodeToLink(temp.Right, ref link);
}
但是我们发现,这样得到的Link是指向双链表最后一个元素22,而我们想要得到的是表头5,为此,我们不得不额外进行while循环,将指针向前移动到表头:
static DoubleLink ReverseDoubleLink(BinNoderoot, ref DoubleLink link)
{
ConvertNodeToLink(root,ref link);
DoubleLink temp = link;
while (temp.Prev != null)
{
temp = temp.Prev;
}
return temp;
}
这么写有点蠢,为什么不直接在递归中就把顺序反转呢?于是有算法2:
算法2:观察算法1的递归方法,访问顺序是Left -> Root –> Right,所以我们要把访问顺序修改为Right -> Root –> Left。
此外,算法的节点访问逻辑,是连接当前节点和新节点,同时指针link向前走,即5=10=11=13=15=16=17=22=link
代码如下所示:
link.Next= new DoubleLink(link, null, root);
link = link.Next;
那么,即使我们颠倒了访问顺序,新的Link也只是变为:22=17=16=15=13=11=10=5=link。
为此,我们修改上面的节点访问逻辑——将Next和Prev属性交换:
link.Prev= new DoubleLink(null, link, root);
link = link.Prev;
这样,新的Link就变成这样的顺序了:link=5=10=11=13=15=16=17=22
算法代码如下所示:
static void ConvertNodeToLink2(BinNoderoot, ref DoubleLink link)
{
if (root == null)
return;
BinNode temp = root;
if (temp.Right != null)
ConvertNodeToLink2(temp.Right, ref link);
//visit current node
link.Prev = new DoubleLink(null, link, root);
link = link.Prev;
if (temp.Left != null)
ConvertNodeToLink2(temp.Left, ref link);
}
以下算法属于二叉树的基本概念,未列出:
1.Huffman Tree的生成、编码和反编码
2.BST的实现
3.Heap的实现,优先队列
4.非平衡二叉树如何变成平衡二叉树?
http://www.cppblog.com/bellgrade/archive/2009/10/12/98402.html
玩二叉树,基本都要用到递归算法。
唉,对于递归函数,我一直纠结,到底要不要返回值?到底先干正事还是先递归?到底要不要破坏原来的数据结构?到底要不要额外做个stack/queue/link/array来转存,还是说完全在递归里面实现?到底终结条件要写成什么样子? ref在递归里面貌似用的很多哦。
==========================================================================================1.把二元查找树转变成排序的双向链表 题目: 输入一棵二元查找树,将该二元查找树转换成一个排序的双向链表。 要求不能创建任何新的结点,只调整指针的指向。 10 /\ 6 14 /\ /\ 4 8 12 16
转换成双向链表 4=6=8=10=12=14=16。 首先我们定义的二元查找树 节点的数据结构如下: struct BSTreeNode { int m_nValue; // value of node BSTreeNode *m_pLeft; // left child of node BSTreeNode *m_pRight; // right child of node };
2.设计包含 min 函数的栈。 定义栈的数据结构,要求添加一个 min 函数,能够得到栈的最小元素。 要求函数 min、push 以及 pop 的时间复杂度都是 O(1)。
3.求子数组的最大和 题目: 输入一个整形数组,数组里有正数也有负数。 数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。 求所有子数组的和的最大值。要求时间复杂度为 O(n)。 例如输入的数组为 1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为 3, 10, -4, 7, 2, 因此输出为该子数组的和 18。
2
4.在二元树中找出和为某一值的所有路径 题目:输入一个整数和一棵二元树。 从树的根结点开始往下访问一直到叶结点所经过的所有结点形成一条路径。 打印出和与输入整数相等的所有路径。 例如 输入整数 22 和如下二元树 10 /\ 5 12 / 4 \ 7
则打印出两条路径:10, 12 和 10, 5, 7。 二元树节点的数据结构定义为: struct BinaryTreeNode // a node in the binary tree { int m_nValue; // value of node BinaryTreeNode *m_pLeft; // left child of node BinaryTreeNode *m_pRight; // right child of node };
5.查找最小的 k 个元素 题目:输入 n 个整数,输出其中最小的 k 个。 例如输入 1,2,3,4,5,6,7 和 8 这 8 个数字,则最小的 4 个数字为 1,2,3 和 4。
第6 题 腾讯面试题: 给你 10 分钟时间,根据上排给出十个数,在其下排填出对应的十个数 要求下排每个数都是先前上排那十个数在下排出现的次数。 上排的十个数如下: 【0,1,2,3,4,5,6,7,8,9】 初看此题,貌似很难,10 分钟过去了,可能有的人,题目都还没看懂。 举一个例子, 数值: 0,1,2,3,4,5,6,7,8,9 分配: 6,2,1,0,0,0,1,0,0,0 0 在下排出现了 6 次,1 在下排出现了 2 次, 2 在下排出现了 1 次,3 在下排出现了 0 次.... 以此类推..
3
第7 题 微软亚院之编程判断俩个链表是否相交 给出俩个单向链表的头指针,比如 h1,h2,判断这俩个链表是否相交。 为了简化问题,我们假设俩个链表均不带环。 问题扩展: 1.如果链表可能有环列 2.如果需要求出俩个链表相交的第一个节点列
第8 题 此贴选一些 比较怪的题, 由于其中题目本身与算法关系不大, , 仅考考思维。 特此并作一题。 1.有两个房间,一间房里有三盏灯,另一间房有控制着三盏灯的三个开关, 这两个房间是 分割开的,从一间里不能看到另一间的情况。 现在要求受训者分别进这两房间一次,然后判断出这三盏灯分别是由哪个开关控制的。 有什么办法呢? 2.你让一些人为你工作了七天,你要用一根金条作为报酬。金条被分成七小块,每天给出一 块。 如果你只能将金条切割两次,你怎样分给这些工人 3. ★用一种算法来颠倒一个链接表的顺序。现在在不用递归式的情况下做一遍。 ★用一种算法在一个循环的链接表里插入一个节点,但不得穿越链接表。 ★用一种算法整理一个数组。你为什么选择这种方法 ★用一种算法使通用字符串相匹配。 ★颠倒一个字符串。优化速度。优化空间。 ★颠倒一个句子中的词的顺序,比如将“我叫克丽丝”转换为“克丽丝叫我”, 实现速度最快,移动最少。 ★找到一个子字符串。优化速度。优化空间。 ★比较两个字符串,用 O(n)时间和恒量空间。 ★假设你有一个用 1001 个整数组成的数组,这些整数是任意排列的,但是你知道所有 的整数都在 1 到 1000(包括 1000)之间。此外,除一个数字出现两次外,其他所有数字只出 现一次。假设你只能对这个数组做一次处理,用一种算法找出重复的那个数字。如果你在运 算中使用了辅助的存储方式,那么你能找到不用这种方式的算法吗 ★不用乘法或加法增加 8 倍。现在用同样的方法增加 7 倍。
4
第9 题 判断整数序列是不是二元查找树的后序遍历结果 题目:输入一个整数数组,判断该数组是不是某二元查找树的后序遍历的结果。 如果是返回 true,否则返回 false。 例如输入 5、7、6、9、11、10、8,由于这一整数序列是如下树的后序遍历结果: / 6 /\ 5 8 \ 10
/\ 7 9 11
因此返回 true。 如果输入 7、4、6、5,没有哪棵树的后序遍历的结果是这个序列,因此返回 false。
第 10 题 翻转句子中单词的顺序。 题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。 句子中单词以空格符隔开。为简单起见,标点符号和普通字母一样处理。 例如输入“I am a student.”,则输出“student. a am I”。 第 11 题 求二叉树中节点的最大距离... 如果我们把二叉树看成一个图,父子节点之间的连线看成是双向的, 我们姑且定义"距离"为两节点之间边的个数。 写一个程序, 求一棵二叉树中相距最远的两个节点之间的距离。
第 12 题 题目:求 1+2+…+n, 要求不能使用乘除法、 for、 while、 、 if else、 switch、 case 等关键字以及条件判断语句 (AB:C) 。
5
第 13 题: 题目:输入一个单向链表,输出该链表中倒数第 k 个结点。 链表的倒数第 0 个结点为链表的尾指针。 链表结点定义如下: struct ListNode { int m_nKey; ListNode* m_pNext; };
第 14 题: 题目:输入一个已经按升序排序过的数组和一个数字, 在数组中查找两个数,使得它们的和正好是输入的那个数字。 要求时间复杂度是 O(n)。如果有多对数字的和等于输入的数字,输出任意一对即可。 例如输入数组 1、2、4、7、11、15 和数字 15。由于 4+11=15,因此输出 4 和 11。
第 15 题: 题目:输入一颗二元查找树,将该树转换为它的镜像, 即在转换后的二元查找树中,左子树的结点都大于右子树的结点。 用递归和循环两种方法完成树的镜像转换。 例如输入: 8 /\ 6 10 /\ /\ 5 7 9 11 输出: 8 /\ 10 6 /\ /\ 11 9 7 5 定义二元查找树的结点为:
6
struct BSTreeNode // a node in the binary search tree (BST) { int m_nValue; // value of node BSTreeNode *m_pLeft; // left child of node BSTreeNode *m_pRight; // right child of node };
第 16 题: 题目(微软) : 输入一颗二元树,从上往下按层打印树的每个结点,同一层中按照从左往右的顺序打印。 例如输入 8 /\ 6 10 /\ /\ 5 7 9 11 输出 8 6 10 5 7 9 11。
第 17 题: 题目:在一个字符串中找到第一个只出现一次的字符。如输入 abaccdeff,则输出 b。 分析:这道题是 2006 年 google 的一道笔试题。 第 18 题: 题目:n 个数字(0,1,…,n-1)形成一个圆圈,从数字 0 开始, 每次从这个圆圈中删除第 m 个数字(第一个为当前数字本身,第二个为当前数字的下一个 数字) 。 当一个数字删除后,从被删除数字的下一个继续删除第 m 个数字。 求出在这个圆圈中剩下的最后一个数字。
第 19 题: 题目:定义 Fibonacci 数列如下: / 0 n=0 f(n)= 1 n=1 \ f(n-1)+f(n-2) n=2
7
输入 n,用最快的方法求该数列的第 n 项。 分析:在很多 C 语言教科书中讲到递归函数的时候,都会用 Fibonacci 作为例子。 因此很多程序员对这道题的递归解法非常熟悉,但....呵呵,你知道的。 。
第 20 题: 题目:输入一个表示整数的字符串,把该字符串转换成整数并输出。 例如输入字符串"345",则输出整数 345。
第 21 题 2010 年中兴面试题 编程求解: 输入两个整数 n 和 m,从数列 1,2,3.......n 中 随意取几个数, 使其和等于 m ,要求将其中所有的可能组合列出来.
第 22 题: 有 4 张红色的牌和 4 张蓝色的牌,主持人先拿任意两张, 再分别在 A、B、C 三人额头上贴任意两张牌, A、B、C 三人都可以看见其余两人额头上的牌, 看完后让他们猜自己额头上是什么颜色的牌, A 说不知道,B 说不知道,C 说不知道,然后 A 说知道了。 请教如何推理,A 是怎么知道的。 如果用程序,又怎么实现呢?
8
第 23 题: 用最简单,最快速的方法计算出下面这个圆形是否和正方形相交。" 3D 坐标系 原点(0.0,0.0,0.0) 圆形: 半径 r = 3.0 圆心 o = (*.*, 0.0, *.*) 正方形: 4 个角坐标; 1:(*.*, 0.0, *.*) 2:(*.*, 0.0, *.*) 3:(*.*, 0.0, *.*) 4:(*.*, 0.0, *.*)
第 24 题: 链表操作, (1).单链表就地逆置, (2)合并链表
第 25 题: 写一个函数,它的原形是 int continumax(char *outputstr,char *intputstr) 功能: 在字符串中找出连续最长的数字串,并把这个串的长度返回, 并把这个最长数字串付给其中一个函数参数 outputstr 所指内存。 例如:"abcd12345ed125ss123456789"的首地址传给 intputstr 后,函数将返回 9, outputstr 所指的值为 123456789
9
26.左旋转字符串 题目: 定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。 如把字符串 abcdef 左旋转 2 位得到字符串 cdefab。请实现字符串左旋转的函数。 要求时间对长度为 n 的字符串操作的复杂度为 O(n),辅助内存为 O(1)。
27.跳台阶问题 题目:一个台阶总共有 n 级,如果一次可以跳 1 级,也可以跳 2 级。 求总共有多少总跳法,并分析算法的时间复杂度。 这道题最近经常出现,包括 MicroStrategy 等比较重视算法的公司 都曾先后选用过个这道题作为面试题或者笔试题。
28.整数的二进制表示中 1 的个数 题目:输入一个整数,求该整数的二进制表达中有多少个 1。 例如输入 10,由于其二进制表示为 1010,有两个 1,因此输出 2。 分析: 这是一道很基本的考查位运算的面试题。 包括微软在内的很多公司都曾采用过这道题。
29.栈的 push、pop 序列 题目:输入两个整数序列。其中一个序列表示栈的 push 顺序, 判断另一个序列有没有可能是对应的 pop 顺序。 为了简单起见,我们假设 push 序列的任意两个整数都是不相等的。 比如输入的 push 序列是 1、2、3、4、5,那么 4、5、3、2、1 就有可能是一个 pop 系列。 因为可以有如下的 push 和 pop 序列: push 1,push 2,push 3,push 4,pop,push 5,pop,pop,pop,pop, 这样得到的 pop 序列就是 4、5、3、2、1。 但序列 4、3、5、1、2 就不可能是 push 序列 1、2、3、4、5 的 pop 序列。
10
30.在从 1 到 n 的正数中 1 出现的次数 题目:输入一个整数 n,求从 1 到 n 这 n 个整数的十进制表示中 1 出现的次数。 例如输入 12,从 1 到 12 这些整数中包含 1 的数字有 1,10,11 和 12,1 一共出现了 5 次。 分析:这是一道广为流传的 google 面试题。
31.华为面试题: 一类似于蜂窝的结构的图,进行搜索最短路径(要求 5 分钟)
32. 有两个序列 a,b,大小都为 n,序列元素的值任意整数,无序; 要求:通过交换 a,b 中的元素,使[序列 a 元素的和]与[序列 b 元素的和]之间的差最小。 例如: var a=[100,99,98,1,2, 3]; var b=[1, 2, 3, 4,5,40];
33. 实现一个挺高级的字符匹配算法: 给一串很长字符串,要求找到符合要求的字符串,例如目的串:123 1******3***2 ,12*****3 这些都要找出来 其实就是类似一些和谐系统。。。 。。 34. 实现一个队列。 队列的应用场景为: 一个生产者线程将 int 类型的数入列,一个消费者线程将 int 类型的数出列
35. 求一个矩阵中最大的二维矩阵(元素和最大).如: 12 03 4 23 45 1 11 53 0 中最大的是: 45 53 要求:(1)写出算法;(2)分析时间复杂度;(3)用 C 写出关键代码
11
第 36 题-40 题(有些题目搜集于 CSDN 上的网友,已标明) : 36.引用自网友:longzuo 谷歌笔试: n 支队伍比赛,分别编号为 0,1,2。。 。。n-1,已知它们之间的实力对比关系, 存储在一个二维数组 w[n][n]中,w[i][j] 的值代表编号为 i,j 的队伍中更强的一支。 所以 w[i][j]=i 或者 j,现在给出它们的出场顺序,并存储在数组 order[n]中, 比如 order[n] = {4,3,5,8,1......},那么第一轮比赛就是 4 对 3, 5 对 8。....... 胜者晋级,败者淘汰,同一轮淘汰的所有队伍排名不再细分,即可以随便排, 下一轮由上一轮的胜者按照顺序,再依次两两比,比如可能是 4 对 5,直至出现第一名 编程实现,给出二维数组 w,一维数组 order 和 用于输出比赛名次的数组 result[n], 求出 result。
37. 有 n 个长为 m+1 的字符串, 如果某个字符串的最后 m 个字符与某个字符串的前 m 个字符匹配, 则两个字符串可以联接, 问这 n 个字符串最多可以连成一个多长的字符串,如果出现循环,则返回错误。
38. 百度面试: 1.用天平(只能比较,不能称重)从一堆小球中找出其中唯一一个较轻的,使用 x 次天平, 最多可以从 y 个小球中找出较轻的那个,求 y 与 x 的关系式。 2.有一个很大很大的输入流,大到没有存储器可以将其存储下来, 而且只输入一次,如何从这个输入流中随机取得 m 个记录。 3.大量的 URL 字符串,如何从中去除重复的,优化时间空间复杂度
12
39. 网易有道笔试: (1). 求一个二叉树中任意两个节点间的最大距离, 两个节点的距离的定义是 这两个节点间边的个数, 比如某个孩子节点和父节点间的距离是 1,和相邻兄弟节点间的距离是 2,优化时间空间复 杂度。 (2). 求一个有向连通图的割点,割点的定义是,如果除去此节点和与其相关的边, 有向图不再连通,描述算法。
40.百度研发笔试题 引用自:zp155334877 1)设计一个栈结构,满足一下条件:min,push,pop 操作的时间复杂度为 O(1)。 2)一串首尾相连的珠子(m 个),有 N 种颜色(N<=10), 设计一个算法,取出其中一段,要求包含所有 N 中颜色,并使长度最短。 并分析时间复杂度与空间复杂度。 3)设计一个系统处理词语搭配问题,比如说 中国 和人民可以搭配, 则中国人民 人民中国都有效。要求: *系统每秒的查询数量可能上千次; *词语的数量级为 10W; *每个词至多可以与 1W 个词搭配 当用户输入中国人民的时候,要求返回与这个搭配词组相关的信息。
41.求固晶机的晶元查找程序 晶元盘由数目不详的大小一样的晶元组成,晶元并不一定全布满晶元盘, 照相机每次这能匹配一个晶元,如匹配过,则拾取该晶元, 若匹配不过,照相机则按测好的晶元间距移到下一个位置。 求遍历晶元盘的算法 求思路。
13
42.请修改 append 函数,利用这个函数实现: 两个非降序链表的并集,1->2->3 和 2->3->5 并为 1->2->3->5 另外只能输出结果,不能修改两个链表的数据。
43.递归和非递归俩种方法实现二叉树的前序遍历。
44.腾讯面试题: 1.设计一个魔方(六面)的程序。 2.有一千万条短信,有重复,以文本文件的形式保存,一行一条,有重复。 请用 5 分钟时间,找出重复出现最多的前 10 条。 3.收藏了 1 万条 url,现在给你一条 url,如何找出相似的 url。 (面试官不解释何为相似)
45.雅虎: 1.对于一个整数矩阵,存在一种运算,对矩阵中任意元素加一时,需要其相邻(上下左右) 某一个元素也加一, 现给出一正数矩阵, 判断其是否能够由一个全零矩阵经过上述运算得到。 2.一个整数数组,长度为 n,将其分为 m 份,使各份的和相等,求 m 的最大值 比如{3,2,4,3,6} 可以分成{3,2,4,3,6} m=1; {3,6}{2,4,3} m=2 {3,3}{2,4}{6} m=3 所以 m 的最大值为 3
46.搜狐: 四对括号可以有多少种匹配排列方式?比如两对括号可以有两种: ()和( ) () ()
47.创新工场: 求一个数组的最长递减子序列 比如{9,4,3,2,5,4,3,2}的最长递减子序列为{9,5,4,3,2}
14
48.微软: 一个数组是由一个递减数列左移若干位形成的, 比如{4,3,2,1,6,5}是由{6,5,4,3,2,1}左移两位形成的, 在这种数组中查找某一个数。
49.一道看上去很吓人的算法面试题: 如何对 n 个数进行排序,要求时间复杂度 O(n),空间复杂度 O(1)
50.网易有道笔试: 1.求一个二叉树中任意两个节点间的最大距离, 两个节点的距离的定义是这两个节点间边的个数, 比如某个孩子节点和父节点间的距离是 1,和相邻兄弟节点间的距离是 2, 优化时间空间复杂度。 2.求一个有向连通图的割点,割点的定义是, 如果除去此节点和与其相关的边,有向图不再连通,描述算法。
51.和为 n 连续正数序列。 题目:输入一个正数 n,输出所有和为 n 连续正数序列。 例如输入 15,由于 1+2+3+4+5=4+5+6=7+8=15,所以输出 3 个连续序列 1-5、4-6 和 7-8。 分析:这是网易的一道面试题。
15
52.二元树的深度。 题目:输入一棵二元树的根结点,求该树的深度。从根结点到叶结点依次经过的结点 (含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 例如:输入二元树: 10 / 6 / 4 输出该树的深度 3。 二元树的结点定义如下: struct SBinaryTreeNode // a node of the binary tree { int m_nValue; // value of node SBinaryTreeNode *m_pLeft; // left child of node SBinaryTreeNode *m_pRight; // right child of node }; 分析:这道题本质上还是考查二元树的遍历。 / 12 \ 14 \ 16
53.字符串的排列。 题目:输入一个字符串,打印出该字符串中字符的所有排列。 例如输入字符串 abc,则输出由字符 a、b、c 所能排列出来的所有字符串 abc、acb、bac、bca、cab 和 cba。 分析:这是一道很好的考查对递归理解的编程题, 因此在过去一年中频繁出现在各大公司的面试、笔试题中。
54.调整数组顺序使奇数位于偶数前面。 题目:输入一个整数数组,调整数组中数字的顺序,使得所有奇数位于数组的前半部分, 所有偶数位于数组的后半部分。要求时间复杂度为 O(n)。
16
55. 题目:类 CMyString 的声明如下: class CMyString { public: CMyString(char* pData = NULL); CMyString(const CMyString& str); ~CMyString(void); CMyString& operator = (const CMyString& str); private: char* m_pData; }; 请实现其赋值运算符的重载函数,要求异常安全,即当对一个对象进行赋值时发生异常, 对象的状态不能改变。
56.最长公共字串。 题目:如果字符串一的所有字符按其在字符串中的顺序出现在另外一个字符串二中, 则字符串一称之为字符串二的子串。 注意,并不要求子串(字符串一)的字符必须连续出现在字符串二中。 请编写一个函数,输入两个字符串,求它们的最长公共子串,并打印出最长公共子串。 例如:输入两个字符串 BDCABA 和 ABCBDAB,字符串 BCBA 和 BDAB 都是是它们的最 长公共子串, 则输出它们的长度 4,并打印任意一个子串。 分析: 求最长公共子串 (Longest Common Subsequence, LCS) 是一道非常经典的动态规划题, 因此一些重视算法的公司像 MicroStrategy 都把它当作面试题。
57.用俩个栈实现队列。 题目:某队列的声明如下: template<typename T> class CQueue {
17
public: CQueue() {} ~CQueue() {} void appendTail(const T& node); void deleteHead(); private: T> m_stack1; T> m_stack2; }; 分析:从上面的类的声明中,我们发现在队列中有两个栈。 因此这道题实质上是要求我们用两个栈来实现一个队列。 相信大家对栈和队列的基本性质都非常了解了:栈是一种后入先出的数据容器, 因此对队列进行的插入和删除操作都是在栈顶上进行;队列是一种先入先出的数据容器, 我们总是把新元素插入到队列的尾部,而从队列的头部删除元素。 // append a element to tail // remove a element from head
58.从尾到头输出链表。 题目:输入一个链表的头结点,从尾到头反过来输出每个结点的值。链表结点定义如下: struct ListNode { int m_nKey; ListNode* m_pNext; }; 分析:这是一道很有意思的面试题。 该题以及它的变体经常出现在各大公司的面试、笔试题中。 59.不能被继承的类。 题目:用 C++设计一个不能被继承的类。 分析:这是 Adobe 公司 2007 年校园招聘的最新笔试题。 这道题除了考察应聘者的 C++基本功底外,还能考察反应能力,是一道很好的题目。
18
60.在 O(1)时间内删除链表结点。 题目:给定链表的头指针和一个结点指针,在 O(1)时间删除该结点。链表结点的定义如下: struct ListNode { int m_nKey; ListNode* m_pNext; }; 函数的声明如下: void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted); 分析:这是一道广为流传的 Google 面试题,能有效考察我们的编程基本功, 还能考察我们的反应速度,更重要的是,还能考察我们对时间复杂度的理解。
61.找出数组中两个只出现一次的数字 题目:一个整型数组里除了两个数字之外,其他的数字都出现了两次。 请写程序找出这两个只出现一次的数字。要求时间复杂度是 O(n),空间复杂度是 O(1)。 分析:这是一道很新颖的关于位运算的面试题。
62.找出链表的第一个公共结点。 题目:两个单向链表,找出它们的第一个公共结点。 链表的结点定义为: struct ListNode { int m_nKey; ListNode* m_pNext; }; 分析:这是一道微软的面试题。 微软非常喜欢与链表相关的题目,因此在微软的面试题中,链表出现的概率相当高。
63.在字符串中删除特定的字符。
19
题目: 输入两个字符串, 从第一字符串中删除第二个字符串中所有的字符。 例如, 输入”They are students.”和”aeiou”,则删除之后的第一个字符串变成”Thy r stdnts.”。 分析:这是一道微软面试题。在微软的常见面试题中,与字符串相关的题目占了很大的一部 分, 因为写程序操作字符串能很好的反映我们的编程基本功。
64. 寻找丑数。 题目:我们把只包含因子 2、3 和 5 的数称作丑数(Ugly Number) 。 例如 6、8 都是丑数,但 14 不是,因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。 求按从小到大的顺序的第 1500 个丑数。 分析:这是一道在网络上广为流传的面试题,据说 google 曾经采用过这道题。
65.输出 1 到最大的 N 位数 题目:输入数字 n,按顺序输出从 1 最大的 n 位 10 进制数。 比如输入 3,则输出 1、2、3 一直到最大的 3 位数即 999。 分析:这是一道很有意思的题目。看起来很简单,其实里面却有不少的玄机。
66.颠倒栈。 题目:用递归颠倒一个栈。例如输入栈{1, 2, 3, 4, 5},1 在栈顶。 颠倒之后的栈为{5, 4, 3, 2, 1},5 处在栈顶。
67.俩个闲玩娱乐。 1.扑克牌的顺子 从扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这 5 张牌是不是连续的。 2-10 为数字本身,A 为 1,J 为 11,Q 为 12,K 为 13,而大小王可以看成任意数字。 2.n 个骰子的点数。 把 n 个骰子扔在地上,所有骰子朝上一面的点数之和为 S。 输入 n,打印出 S 的所有可能的值出现的概率。
20
68.把数组排成最小的数。 题目:输入一个正整数数组,将它们连接起来排成一个数,输出能排出的所有数字中最小的 一个。 例如输入数组{32, 321},则输出这两个能排成的最小数字 32132。 请给出解决问题的算法,并证明该算法。 分析:这是 09 年 6 月份百度的一道面试题, 从这道题我们可以看出百度对应聘者在算法方面有很高的要求。
69.旋转数组中的最小元素。 题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个排好序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3, 4, 5, 1, 2}为{1, 2, 3, 4, 5}的一个旋转,该数组的最小值为 1。 分析:这道题最直观的解法并不难。从头到尾遍历数组一次,就能找出最小的元素, 时间复杂度显然是 O(N)。但这个思路没有利用输入数组的特性,我们应该能找到更好的解 法。
70.给出一个函数来输出一个字符串的所有排列。 分析:简单的回溯就可以实现了。当然排列的产生也有很多种算法,去看看组合数学, 还有逆序生成排列和一些不需要递归生成排列的方法。 印象中 Knuth 的<TAOCP>第一卷里面深入讲了排列的生成。 这些算法的理解需要一定的数学功底, 也需要一定的灵感,有兴趣最好看看。
71.数值的整数次方。 题目:实现函数 double Power(double base, int exponent),求 base 的 exponent 次方。 不需要考虑溢出。 分析: 这是一道看起来很简单的问题。 可能有不少的人在看到题目后 30 秒写出如下的代码: double Power(double base, int exponent) { double result = 1.0; for(int i = 1; i <= exponent; ++i)
21
result *= base; return result; }
72. 题目:设计一个类,我们只能生成该类的一个实例。 分析:只能生成一个实例的类是实现了 Singleton 模式的类型。
73.对策字符串的最大长度。 题目:输入一个字符串,输出该字符串中对称的子字符串的最大长度。 比如输入字符串“google”,由于该字符串里最长的对称子字符串是“goog”,因此输出 4。 分析: 可能很多人都写过判断一个字符串是不是对称的函数, 这个题目可以看成是该函数的 加强版。
74.数组中超过出现次数超过一半的数字 题目:数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。 分析:这是一道广为流传的面试题,包括百度、微软和 Google 在内的多家公司 都曾经采用过这个题目。 要几十分钟的时间里很好地解答这道题, 除了较好的编程能力之外, 还需要较快的反应和较强的逻辑思维能力。
75.二叉树两个结点的最低共同父结点 题目:二叉树的结点定义如下: struct TreeNode { int m_nvalue; TreeNode* m_pLeft; TreeNode* m_pRight; }; 输入二叉树中的两个结点,输出这两个结点在数中最低的共同父结点。 分析: 求数中两个结点的最低共同结点是面试中经常出现的一个问题。 这个问题至少有两个
22
变种。
76.复杂链表的复制 题目:有一个复杂链表,其结点除了有一个 m_pNext 指针指向下一个结点外, 还有一个 m_pSibling 指向链表中的任一结点或者 NULL。其结点的 C++定义如下: struct ComplexNode { int m_nValue; ComplexNode* m_pNext; ComplexNode* m_pSibling; }; 下图是一个含有 5 个结点的该类型复杂链表。 图中实线箭头表示 m_pNext 指针,虚线箭头表示 m_pSibling 指针。 为简单起见,指向 NULL 的指针没有画出。 请完成函数 ComplexNode* Clone(ComplexNode* pHead),以复制一个复杂链表。 //图,接下来,自会补上。July、11.19. 分析:在常见的数据结构上稍加变化,这是一种很新颖的面试题。 要在不到一个小时的时间里解决这种类型的题目, 我们需要较快的反应能力,对数据结构透彻的理解以及扎实的编程功底。
77.关于链表问题的面试题目如下: 题一、 给定单链表,检测是否有环。 使用两个指针 p1,p2 从链表头开始遍历,p1 每次前进一步,p2 每次前进两步。 如果 p2 到达链表尾部,说明无环,否则 p1、p2 必然会在某个时刻相遇(p1==p2),从而检测 到链表中有环。
题二、 给定两个单链表(head1, head2),检测两个链表是否有交点,如果有返回第一个交点。 如果 head1==head2,那么显然相交,直接返回 head1。 否则,分别从 head1,head2 开始遍历两个链表获得其长度 len1 与 len2,假设 len1>=len2, 那么指针 p1 由 head1 开始向后移动 len1-len2 步,指针 p2=head2, 下面 p1、p2 每次向后前进一步并比较 p1p2 是否相等,如果相等即返回该结点, 否则说明两个链表没有交点。 题三、 给定单链表(head),如果有环的话请返回从头结点进入环的第一个节点。 运用题一,我们可以检查链表中是否有环。
23
如果有环,那么 p1p2 重合点 p 必然在环中。从 p 点断开环,方法为:p1=p, p2=p->next, p->next=NULL。此时,原单链表可以看作两条单链表,一条从 head 开始,另一条从 p2 开 始, 于是运用题二的方法,我们找到它们的第一个交点即为所求。 题四、只给定单链表中某个结点 p(并非最后一个结点,即 p->next!=NULL)指针,删除该结 点。 办法很简单,首先是放 p 中数据,然后将 p->next 的数据 copy 入 p 中,接下来删除 p->next 即可。 题五、只给定单链表中某个结点 p(非空结点),在 p 前面插入一个结点。 办法与前者类似,首先分配一个结点 q,将 q 插入在 p 后, 接下来将 p 中的数据 copy 入 q 中,然后再将要插入的数据记录在 p 中。
78.链表和数组的区别在哪里? 分析:主要在基本概念上的理解。 但是最好能考虑的全面一点,现在公司招人的竞争可能就在细节上产生, 谁比较仔细,谁获胜的机会就大。
79. 1.编写实现链表排序的一种算法。说明为什么你会选择用这样的方法? 2.编写实现数组排序的一种算法。说明为什么你会选择用这样的方法? 3.请编写能直接实现 strstr()函数功能的代码。
80.阿里巴巴一道笔试题 引自 baihacker 问题描述: 12 个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高, 问排列方式有多少种 这个笔试题,很 YD,因为把某个递归关系隐藏得很深. //第 81-100 题正在整理中(完) 。
====================================================================================
数据结构与算法,这个部分的内容其实是十分的庞大,要想都覆盖到不太容易。在校学习阶段我们可能需要对每种结构,每种算法都学习,但是找工作笔试或者面试的时候,要在很短的时间内考察一个人这方面的能力,把每种结构和算法都问一遍不太现实。所以,实际的情况是,企业一般考察一些看起来很基本的概念和算法,或者是一些变形,然后让你去实现。也许看起来简单,但是如果真让你在纸上或者是计算机上快速地完成一个算法,并且设计测试案例,最后跑起来,你就会发现会很难了。这就要求我们要熟悉,并牢固掌握常用的算法,特别是那些看起来貌似简单的算法,正是这些用起来很普遍的算法,才要求我们能很扎实的掌握,在实际工作中提高工作效率。遇到复杂的算法,通过分析和扎实的基本功,应该可以很快地进行开发。
闲话少说,下面进入正题。
一.数据结构部分
1.数组和链表的区别。(很简单,但是很常考,记得要回答全面)
C++语言中可以用数组处理一组数据类型相同的数据,但不允许动态定义数组的大小,即在使用数组之前必须确定数组的大小。而在实际应用中,用户使用数组之前有时无法准确确定数组的大小,只能将数组定义成足够大小,这样数组中有些空间可能不被使用,从而造成内存空间的浪费。链表是一种常见的数据组织形式,它采用动态分配内存的形式实现。需要时可以用new分配内存空间,不需要时用delete将已分配的空间释放,不会造成内存空间的浪费。
从逻辑结构来看:数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况,即数组的大小一旦定义就不能改变。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项)。
从内存存储来看:(静态)数组从栈中分配空间(用NEW创建的在堆中), 对于程序员方便快速,但是自由度小;链表从堆中分配空间, 自由度大但是申请管理比较麻烦.
从访问方式来看:数组在内存中是连续存储的,因此,可以利用下标索引进行随机访问;链表是链式存储结构,在访问元素的时候只能通过线性的方式由前到后顺序访问,所以访问效率比数组要低。
2.链表的一些操作,如链表的反转,链表存在环路的判断(快慢指针),双向链表,循环链表相关操作。
3.队列(特殊的如优先级队列),栈的应用。(比如队列用在消息队列,栈用在递归调用中)
4.二叉树的基本操作
二叉树的三种遍历方式(前序,中序,后序)及其递归和非递归实现,三种遍历方式的主要应用(如后缀表达式等)。相关操作的时间复杂度。
5.字符串相关
整数,浮点数和字符串之间的转换(atoi,atof,itoa)
字符串拷贝注意异常检查,比如空指针,字符串重叠,自赋值,字符串结束符'/0'等。
二.算法部分
1.排序算法:
排序可以算是最基本的,最常用的算法,也是笔试面试中最常被考察到的算法。最基本的冒泡排序,选择排序,插入排序要可以很快的用代码实现,这些主要考察你的实际编码能力。堆排序,归并排序,快排序,这些算法需要熟悉主要的思想,和需要注意的细节地方。需要熟悉常用排序算法的时间和空间复杂度。
各种排序算法的使用范围总结:(1)当数据规模较小的时候,可以用简单的排序算法如直接插入排序或直接选择排序。(2)当文件的初态已经基本有序时,可以用直接插入排序或冒泡排序。(3)当数据规模比较大时,应用速度快的排序算法。可以考虑用快速排序。当记录随机分布的时候,快排的平均时间最短,但可能出现最坏的情况,这时候的时间复杂度是O(n^2),且递归深度为n,所需的栈空间问O(n)。(4)堆排序不会出现快排那样的最坏情况,且堆排序所需的辅助空间比快排要少。但这两种算法都不是稳定的,若要求排序时稳定的,可以考虑用归并排序。(5)归并排序可以用于内排序,也可以用于外排序。在外排序时,通常采用多路归并,并且通过解决长顺串的合并,产生长的初始串,提高主机与外设并行能力等措施,以减少访问外存额次数,提高外排序的效率。
2,查找算法
能够熟练写出或者是上机编码出二分查找的程序。
3.hash算法
4.一些算法设计思想。
贪心算法,分治算法,动态规划算法,随机化算法,回溯算法等。这些可以根据具体的例子程序来复习。
5.STL
STL(Standard Template Library)是一个C++领域中,用模版技术实现的数据结构和算法库,已经包含在了C++标准库中。其中的vecor,list,stack,queue等结构不仅拥有更强大的功能,还有了更高的安全性。除了数据结构外,STL还包含泛化了的迭代器,和运行在迭代器上的各种实用算法。这些对于对性能要求不是太高,但又不希望自己从底层实现算法的应用还是很具有诱惑力的。