数据结构
1. 链表与数组
单向链表 双向链表 顺序表
1.1 链表的方向
思路1:重新开辟内存,新建链表,然后从后面往前面复制。时间复杂度则为O(n^2),空间复杂度为O(n)。
思路2:不开辟新内存,采用交换节点内容的方式。前后两个指针,相互交换内容,之后前面的指针后移,后面的指针前移,再交换(类似冒泡)。时间复杂度O(n^2)。
思路3:改变指针指向。时间复杂度为O(n),空间复杂度为O(1)。
1.2 删除单链表中的重复值
思路:使用哈希表,从头扫描,将出现过的节点存入哈希表中。如果元素已经在哈希表中出现过则删除,没有则存入。
1.3 判断单链表是否有环
思路:快慢指针,快指针每次走两步,慢指针每次走一步。 每次判断快指针是否到头了以及快慢指针是否指向同一元素。 快指针走到头了,则没有环; 如果快指针和慢指针指向同一个元素,则有环。
1.4 找到环的起始点
思路:如果有环,则快慢指针相遇一定会在环中,通过相遇点拆开环,这时候有两个单链表,找这两个单链表的相交点就是起点。
其他
思路:采用快慢指针的方法,可以找到中间节点,找环,判断相交等。
2. 队列和栈
2.1 实现一个栈,要求实现出栈,入栈,Min返回最小值的操作的时间复杂度为o(1)
思路:正常实现的出栈入栈满足时间复杂度为O(1),但是如果按照正常的实现方式,Min返回最小值的操作需要遍历一遍数据,时间复杂度为O(N)。此时,可以用两个栈,一个正常存放数据,另一个栈顶端存放最小值,在求解MIn的时候,直接让最小值栈出栈就可以。
2.2 用两个栈实现一个队列
思路:定义两个栈st1,st2,st1栈专门入数据,st2栈专门出数据。无论st1有没有数据直接入数据,如果st2有数据则出数据,没有数据则将st1的数据“倒入”st2中
2.3 用两个队列实现一个栈
思路:一个队列q1专门负责入数据,只要有数据就入,将队列q1中的元素入到辅助队列q2中,直到q1中只剩下最后一个元素,将这个元素出队列,即实现了出栈
3. 字符串操作
部分搬运,此处只罗列知识点及自己的想法,具体链接面试系列 字符串处理算法
序列和子串的区别:序列不要求连续,子串要求
3.1 最大子序列和
普通求解: 设置start和end,求解start与end之间的和,若大于先前计算的序列和,则该start,end对是新的序列和的起始位置和结束位置,遍历所有可能的start,end对。时间复杂度为O(n^2)
动态规划:见链接;
分治法:将一个序列分成left,right,middle三部分,用递归的方式求解left和right的最大序列和left_max和right_max,求解[left,middle]之间的从middle出发的最大值left_middle_max,以及[middle,right]之间的从middle出发的最大值right_middle_max,求得 middle_max = left_middle_max+ right_middle_max,
left_max,right_max和middle_max 中最大值作为序列[left,right]之间最大子序列和。
3.2 最长递增子序列 (LIS,Longest Increasing Subsequence)
问题描述:设序列L=a1, a2, a3, …, an是长度为n的序列,L的一个递增序列描述为:ai1, ai2,…, aik, 其中下标序列 i1, i2, …, ik是递增的, 子序列ai1, ai2, …., aik 也是递增的。此递增序列的长度为 k。
思路一:【转化成最长公共子串问题】将序列L进行排序,求解得到排序后的L*,然后求解L和L*的最长公共子串。
思路二:【动态规划】用lStr[i] 表示以第 i 个元素为结尾的最长递增子序列,最后求出 lStr[i]中长度最长的序列就是最长递增子序列,实例如下Arr ={1,2,3,7,5,4,8,9}
lStr[1]:1
lStr[2]:1,2
lStr[3]:1,2,3
lStr[4]:1,2,3,7
lStr[5]:1,2,3,5
lStr[6]:1,2,3,4
lStr[7]:1,2,3,7,8 / 1,2,3,5,8 / 1,2,3,4,8
lStr[8] :1,2,3,7,8,9 / 1,2,3,5,8,9 / 1,2,3,4,8,9
在求解第I个最长递增子序列的时候,找出比Arr[I]小的且IStr序列最长的递增子序列。
比如在求解IStr[6]的时候,Arr[6]=4,小于Arr[6]的是Arr[1],Arr[2],Arr[3],对应的IStr[1]=1,IStr[2]=1,2,IStr[3]=1,2,3,其中长度最长的是IStr[3],因此IStr[6]=IStr[3]+Arr[6]
3.3 最长公共子串 (LCS,Longest Common SubString)
思路:矩阵法求解,求解abcaba和cabbcab的最长公共子串。
- | a | b | c | a | b | a |
---|---|---|---|---|---|---|
c | 0 | 0 | 1 | 0 | 0 | 0 |
a | 1 | 0 | 0 | 1 | 0 | 1 |
b | 0 | 1 | 0 | 0 | 1 | 0 |
b | 0 | 1 | 0 | 0 | 1 | 0 |
c | 0 | 0 | 1 | 0 | 0 | 0 |
a | 1 | 0 | 0 | 1 | 0 | 1 |
b | 0 | 1 | 0 | 0 | 1 | 0 |
搜寻连续的最长斜边,因此最长子串是bcab。
为减少搜索的时间复杂度,对标记方式进行修改。
- | a | b | c | a | b | a |
---|---|---|---|---|---|---|
c | 0 | 0 | 1 | 0 | 0 | 0 |
a | 1 | 0 | 0 | 2 | 0 | 1 |
b | 0 | 2 | 0 | 0 | 3 | 0 |
b | 0 | 1 | 0 | 0 | 1 | 0 |
c | 0 | 0 | 2 | 0 | 0 | 0 |
a | 1 | 0 | 0 | 3 | 0 | 1 |
b | 0 | 2 | 0 | 0 | 4 | 0 |
标记的方式是斜边上的数字+1,因此最长子串的长度是4,序列为bcab
3.4 最长公共子序列(Longest Common Subsequence)
思路:同样适用矩阵法来求解。
问题定义:两个字符串Str1,Str2,矩阵A[i][j]表示Str1中以i位结束字符串与Str2中以j结束的字符串的最长公共子序列。
生成矩阵
当Str1[i]==Str2[j]时,A[i][j] = A[i-1][j-1] +1
当Str1[i]!=Str2[j]时,A[i][j] = max{A[i][j-1],A[i-1][j]}
- | b | d | c | a | b | a |
---|---|---|---|---|---|---|
a | 0 | 0 | 0 | 1 | 0 | 1 |
b | 1 | 1 | 1 | 1 | 2 | 2 |
c | 1 | 1 | 2 | 2 | 2 | 2 |
b | 1 | 1 | 2 | 2 | 3 | 3 |
d | 1 | 2 | 2 | 2 | 3 | 3 |
a | 1 | 2 | 2 | 3 | 3 | 4 |
b | 1 | 2 | 2 | 3 | 4 | 4 |
矩阵检索
i和j分别从m,n开始,递减循环直到i = 0,j = 0。其中,m和n分别为两个串的长度。
当Str1[i] == Str2[j],则将str1[i]字符插入到子序列内,i–,j–;
当Str1[i] != Str2[j],则比较A[i,j-1]与A[i-1,j],A[i,j-1]大,则j–,否则i–;(如果相等,则任选一个)
3.5 最长不重复子串
3.6 最长重复子串
问题描述:首先这是一个单字符串问题。子字符串R 在字符串L 中至少出现两次,则称R 是L 的重复子串。重复子串又分为可重叠重复子串和不可重叠重复子串。
暴力求解:枚举所有子串,然后子串去匹配。
KMP:
朴素的KMP
思路:假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:如果当前字符匹配Target[i]==Pattern[j],则i++,j++继续下一个字符的匹配;如果匹配失败,则i=i-(j-1),j=0。相当于每次匹配失败时,j回溯到其实位置。
结合next数组的KMP
next数组求解
void GetNext(char* p,int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1){
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p[j] == p[k]){
k++;
j++;
next[j] = k;
/*
//将next[j]=k;替换成下面的代码,避免了next中k的回溯
if(P[j]!=P[k]){
next[j]=k;
}else
next[j]=next[k];
*/
}
else
k = next[k];
}
}
实例
- | a | b | c | d | a | b | c | e | x |
---|---|---|---|---|---|---|---|---|---|
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
next[i] | -1 | 0 | 0 | 0 | 0 | 1 | 2 | 3 |
k=-1;j=0;next[0]=-1;
k++;j++;next[j]=k;(k==-1) ==>> k=0;j=1;next[1]=0;
p[j] != p[k] ==>> k=next[k]=-1;j=1;
k++;j++;next[j]=k; (k==-1) ==>> k=0;j=2;next[2]=0;
p[j] != p[k] ==>> k=next[k]=-1;j=2;
k++;j++;next[j]=k; (k==-1) ==>> k=0;j=3;next[3]=0;
p[j] != p[k] ==>> k=next[k]=-1;j=3;
k++;j++;next[j]=k; (k==-1) ==>> k=0;j=4;next[4]=0;
p[j] == p[k] ==>> k=1;j=5;next[5]=1;
p[j] == p[k] ==>> k=2;j=6;next[6]=2;
p[j] == p[k] ==>> k=3;j=7;next[7]=3;
匹配方式:如果Target[j]!=Pattern[i],Target向后移动i-next[i]步,即j=j+i-1,i=next[i];
结合好后缀的KMP(BM) (以后再补充了)
3.7 最长不重复子序列
问题描述:从一个字符串中找到一个连续子串,该子串中任何两个字符不能相同,求子串的最大长度并输出一条最长不重复子串。
基本方法:遍历每个字符起始的子串,辅助hash,寻求最长的不重复子串,时间复杂度为O(n^2)
/* LNRS 基本算法 hash */
char visit[256];
void LNRS_hash(char * arr, int size) {
int i, j;
for(i = 0; i < size; ++i){
memset(visit,0,sizeof visit);//清空hash区
visit[arr[i]] = 1;//从第i个数据开始往后开始
for(j = i+1; j < size; ++j){
if(visit[arr[j]] == 0){//如果之前的数据没有,则设置成1
visit[arr[j]] = 1;
}
else{//与之前的重复
if(j-i > maxlen){
maxlen = j - i;
maxindex = i;
}
break;
}
}
if((j == size) && (j-i > maxlen)){
maxlen = j - i;
maxindex = i;
}
}
output(arr);
}
动态规划:对于最长不重复子串,某个当前的字符,如果它与前面的最长不重复子串中的字符没有重复,那么就可以以它为结尾构成新的最长子串;如果有重复,且重复位置在上一个最长子串起始位置之后,那么就与该起始位置之后的稍短的子串构成新的子串或者单独成一个新子串。
/* LNRS dp */
int dp[30];//存储下标为i的串的最长不重复子串的长度
void LNRS_dp(char * arr, int size){
int i, j;
int last_start = 0; // 上一次最长子串的起始位置
maxlen = maxindex = 0;
dp[0] = 1;
for(i = 1; i < size; ++i){
//从i-1往回遍历知道last_start
for(j = i-1; j >= last_start; --j){
//遍历过程中发现有重复,这需要修正dp[i]和last_start
if(arr[j] == arr[i]){
dp[i] = i - j;
last_start = j+1; // 更新last_start
break;
}
//没有重复
else if(j == last_start){
dp[i] = dp[i-1] + 1;
}
}
if(dp[i] > maxlen){
maxlen = dp[i];
maxindex = i + 1 - maxlen;
}
}
output(arr);
}
时间复杂度仍然是O(n^2),因为有回溯的过程。
动态规划+hash:通过hash表来记录已经出现过数据的下标。
void LNRS_dp_hash(char * arr, int size) {
memset(visit, -1, sizeof visit);//visit数组保存已经被访问过的下标
memset(dp, 0, sizeof dp);//清空dp
maxlen = maxindex = 0;
dp[0] = 1;
visit[arr[0]] = 0;
int last_start = 0;
for(int i = 1; i < size; ++i){
//如果之前序列中没有出现过,则记录下下标
if(visit[arr[i]] == -1){
dp[i] = dp[i-1] + 1;
visit[arr[i]] = i;
}
//有重复
else{
//重复的位置在 last_start,i之间
if(last_start <= visit[arr[i]]){
dp[i] = i - visit[arr[i]];//以i为末尾的最长子串出现的位置是重复位置到i
last_start = visit[arr[i]] + 1;//更新起始位置
visit[arr[i]] = i;
}
//重复位置在last_start之前
else{
dp[i] = dp[i-1] + 1;
visit[arr[i]] = i; /* 更新最近重复位置 */
}
}
if(dp[i] > maxlen) {
maxlen = dp[i];
maxindex = i + 1 - maxlen;
}
}
output(arr);
}
时间复杂度为O(n)
动态规划+hash优化:优化空间复杂度,dp[i-1]的作用只是更新dp[i],因此没有必要用一个数组来保存,只需要一个变量来保存上是时刻的长度就好。
void LNRS_dp_hash_impro(char * arr, int size){
memset(visit, -1, sizeof visit);
maxlen = maxindex = 0;
visit[arr[0]] = 0; //visit数组保存已经被访问过的下标
int curlen = 1;//保存之前的长度
int last_start = 0;
for(int i = 1; i < size; ++i){
//如果之前序列中没有出现过,则记录下下标
if(visit[arr[i]] == -1){
++curlen;
visit[arr[i]] = i;
}
//有重复
else{
//重复的位置在 last_start,i之间
if(last_start <= visit[arr[i]]){
curlen = i - visit[arr[i]];
last_start = visit[arr[i]] + 1;
visit[arr[i]] = i;
}
//重复位置在last_start之前
else{
++curlen;
visit[arr[i]] = i;
}
}
if(curlen > maxlen){
maxlen = curlen;
maxindex = i + 1 - maxlen;
}
}
output(arr);
}