1 数组、字符串【Array、String】
1.1 字符串转化
数组和字符串是最基本的数据结构,在不少编程语言中都有着十分类似的性质,而围绕着它们的算法面试题也是最多的。html
不少时候,在分析字符串相关面试题的过程当中,咱们每每要针对字符串当中的每个字符进行分析和处理,甚至有时候咱们得先把给定的字符串转换成字符数组以后再进行分析和处理。node
举例:翻转字符串“algorithm”。面试
解法:用两个指针,一个指向字符串的第一个字符a,一个指向它的最后一个字符m,而后互相交换。交换以后,两个指针向中央一步步地靠拢并相互交换字符,直到两个指针相遇。这是一种比较快速和直观的方法。...算法
//翻转字符串“algorithm” int main() { char temp,a[] = "algorithm"; int i,j,length = strlen(a); temp = NULL; i = 0; j = length - 1; while(i != j) { temp = a[i]; a[i] = a[j]; a[j] = temp; i++; j--; } for(i = 0;i < length;i++) { printf("%c\t",a[i]); } return 0; }![]()
1.2数组的优缺点
数组的优势在于:编程
构建很是简单数组
能在O(1)的时间里根据数组的下标(index)查询某个元素数据结构
而数组的缺点在于:编程语言
构建时必须分配一段连续的空间优化
查询某个元素是否存在时须要遍历整个数组,耗费 O(n) 的时间(其中,n 是元素的个数)spa
删除和添加某个元素时,一样须要耗费 O(n) 的时间
1.3【242】有效的字母异位词
字母异位词:
也就是两个字符串中的相同字符的数量要对应相等。例如,s等于“anagram”,t等于“nagaram”,s和t就互为字母异位词。由于它们都包含有三个字符a,一个字符g,一个字符 m,一个字符 n,以及一个字符 r。而当 s 为 “rat”,t 为 “car”的时候,s 和 t 不互为字母异位词。
解题思路:
一个重要的前提“假设两个字符串只包含小写字母”,小写字母一共也就26个,所以:
知识点:哈希映射
能够利用两个长度都为26的字符数组来统计每一个字符串中小写字母出现的次数,而后再对比是否相等;
#include <stdio.h> #include <stdlib.h> #include <string.h> int isAnagram(char * s, char * t) { //判断两个字符串长度是否相等,不相等则直接返回 false if(strlen(s) != strlen(t)) return 0; //若相等,则初始化 26 个字母哈希表,遍历字符串 s 和 t int A[26] = {0},B[26] = {0}; //哈希映射 int i; while(*s != '\0') { A[*s - 'a']++; B[*t - 'a']++; s++; t++; } //判断两个表是否相同 for(i = 0; i < 26; i++) { if(A[i] != B[i]) return 0; } return 1; } int main() { char b[] = "nagaram",a[] = "anagram"; if(isAnagram(a,b) != 0) printf("True"); else printf("False"); return 0; }能够只利用一个长度为 26 的字符数组,将出如今字符串 s 里的字符个数加 1,而出如今字符串 t 里的字符个数减 1,最后判断每一个小写字母的个数是否都为 0。
#include <stdio.h> #include <stdlib.h> #include <string.h> int isAnagram(char * s, char * t) { //判断两个字符串长度是否相等,不相等则直接返回 false if(strlen(s) != strlen(t)) return 0; //若相等,则初始化 26 个字母哈希表,遍历字符串 s 和 t int A[26] = {0}; //哈希映射 int i; while(*s != '\0\) { //s 负责在对应位置增长,t 负责在对应位置减小 A[*s - 'a']++; A[*t - 'a']--; s++; t++; } //若是哈希表的值都为 0,则两者是字母异位词 for(i = 0; i < 26; i++) { if(A[i] != 0) return 0; } return 1; } int main() { char b[] = "nagaram",a[] = "anagram"; if(isAnagram(a,b) != 0) printf("True"); else printf("False"); return 0; }2 链表(LinkedList)
单链表:链表中的每一个元素其实是一个单独的对象,而全部对象都经过每一个元素中的引用字段连接在一块儿。
双链表:与单链表不一样的是,双链表的每一个结点中都含有两个引用字段。
2.1链表的优缺点
链表的优势以下:
链表能灵活地分配内存空间;
能在O(1)时间内删除或者添加元素,前提是该元素的前一个元素已知,固然也取决因而单链表仍是双链表,在双链表中,若是已知该元素的后一个元素,一样能够在 O(1) 时间内删除或者添加该元素。
链表的缺点是:
不像数组能经过下标迅速读取元素,每次都要从链表头开始一个一个读取;
查询第 k 个元素须要 O(k) 时间。
2.2 应用场景
若是要解决的问题里面须要不少快速查询,链表可能并不适合;若是遇到的问题中,数据的元素个数不肯定,并且须要常常进行数据的添加和删除,那么链表会比较合适。而若是数据元素大小肯定,删除插入的操做并
很少,那么数组可能更适合。
2.3 经典解法
2.3.1 利用快慢指针(有时候须要用到三个指针)
典型题目例如:链表的翻转,寻找倒数第k个元素,寻找链表中间位置的元素,判断链表是否有环等等。
2.3.2 .构建一个虚假的链表头
通常用在要返回新的链表的题目中,好比,给定两个排好序的链表,要求将它们整合在一块儿并排好序。又好比,将一个链表中的奇数和偶数按照原定的顺序分开后从新组合成一个新的链表,链表的头一半是奇数,后一半是偶数。
在这类问题里,若是不用一个虚假的链表头,那么在建立新链表的第一个元素时,咱们都得要判断一下链表的头指针是否为空,也就是要多写一条ifelse语句。比较简洁的写法是建立一个空的链表头,直接往其后面
添加元素便可,最后返回这个空的链表头的下一个节点便可。
建议:在解决链表的题目时,能够在纸上或者白板上画出节点之间的相互关系,而后画出修改的方法
2.4 【25】K个一组翻转链表
解题思路:
这道题考察了两个知识点:
对链表翻转算法是否熟悉
对递归算法的理解是否清晰
在翻转链表的时候,能够借助三个指针:prev、curr、next,分别表明前一个节点、当前节点和下一个节点,实现过程以下所示:
【递归】方法1:
- 一、找到待翻转的k个节点(注意:若剩余数量小于k的话,则不须要反转,所以直接返回待翻转部分的头结点便可)。
- 二、对其进行翻转。并返回翻转后的头结点(注意:翻转为左闭又开区间,因此本轮操做的尾结点其实就是下一轮操做的头结点)。
- 三、对下一轮k个节点也进行翻转操做。
- 四、将上一轮翻转后的尾结点指向下一轮翻转后的头节点,即将每一轮翻转的k的节点链接起来。
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct slist { int data; struct slist *next; }; struct slist *reverse(struct slist *head,struct slist *tail) { struct slist *pre = NULL; struct slist *next = NULL; while(head != tail) { next = head ->next; head ->next = pre; pre = head; head = next; } return pre; } struct slist *reverseKGroup(struct slist* head, int k) { if(head == NULL || head ->next == NULL) return head; struct slist *newHead,*tail = head; int i; for(i = 0;i < k;i++) { //剩余数量小于k的话,不须要反转 if(tail == NULL) return head; tail = tail ->next; } //反转前K个元素 newHead = reverse(head,tail); //下一轮的开始的地方就是tail head ->next = reverseKGroup(tail,k); return newHead; } void input(struct slist *head) { struct slist *p = head ->next; //p是工做指针 while(p != NULL) { printf("%d\t",p ->data); p = p ->next; } } void create(struct slist *head) { //尾插法创建单链表 struct slist *r,*temp; //r是尾指针,temp是临时结点 int i,x; r = head; printf("请输入元素:\n"); scanf("%d",&x); while(x != 9999) { temp = (struct slist *)malloc(sizeof(struct slist)); temp ->data = x; temp ->next = r ->next; r ->next = temp; r = temp; scanf("%d",&x); } } int main() { struct slist *head;//head是头结点 head = (struct slist *)malloc(sizeof(struct slist)); head ->next = NULL; create(head); input(head); int k; printf("\n请输入K:"); scanf("%d",&k); head ->next= reverseKGroup(head ->next,k); input(head); return 0; }【递归】方法2:
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct slist { int data; struct slist *next; }; struct slist *reverseKGroup(struct slist* head, int k) { struct slist *cur = head; int count = 0; // 找到待翻转的k个节点 while(cur != NULL && count != k) { cur = cur ->next; count++; } if(count == k) { cur = reverseKGroup(cur,k); while(count != 0) { count--; struct slist *tmp = head ->next; head ->next = cur; cur = head; head = tmp; } head = cur; } //若剩余数量小于k的话,则不须要反转,所以直接返回待翻转部分的头结点便可 return head; //head为头指针 } void input(struct slist *head) { struct slist *p = head ->next; //p是工做指针 while(p != NULL) { printf("%d\t",p ->data); p = p ->next; } } void create(struct slist *head) { //尾插法创建单链表 struct slist *r,*temp; //r是尾指针,temp是临时结点 int i,x; r = head; printf("请输入元素:\n"); scanf("%d",&x); while(x != 9999) { temp = (struct slist *)malloc(sizeof(struct slist)); temp ->data = x; temp ->next = r ->next; r ->next = temp; r = temp; scanf("%d",&x); } } int main() { struct slist *head;//head是头结点 head = (struct slist *)malloc(sizeof(struct slist)); head ->next = NULL; create(head); input(head); int k; printf("\n请输入K:"); scanf("%d",&k); head ->next= reverseKGroup(head ->next,k); input(head); return 0; }3 栈(Stack)
3.1 特色
栈的最大特色就是后进先出(LIFO)。对于栈中的数据来讲,全部操做都是在栈的顶部完成的,只能够查看栈顶部的元素,只可以向栈的顶部压⼊数据,也只能从栈的顶部弹出数据。
3.2 实现
利用一个单链表来实现栈的数据结构。并且,由于咱们都只针对栈顶元素进行操做,因此借用单链表的头就能让全部栈的操做在O(1)的时间内完成。
3.3 应用场景
在解决某个问题的时候,只要求关心最近一次的操做,而且在操做完成了以后,须要向前查找到更前一次的操做。
若是打算用一个数组外加一个指针来实现类似的效果,那么,一旦数组的长度发生了改变,哪怕只是在最后添加一个新的元素,时间复杂度都再也不是 O(1),并且,空间复杂度也得不到优化。
3.4【20】有效的括号
解题思路
利用一个栈,不断地往里压左括号,一旦赶上了一个右括号,咱们就把栈顶的左括号弹出来,表示这是一个合法的组合,以此类推,直到最后判断栈里还有没有左括号剩余。
#include <stdio.h> #include <stdlib.h> #include <string.h> #define Max 20 #define Stack char int isValid(char * p) { int len=strlen(p); if(len == 1) return 0; int top=1,i; Stack s[Max]; //堆栈存储 for(i=0; i<len; i++) { switch(p[i]) { case '(': case '[': case '{': s[top++]=p[i]; //进栈 break; case ')': if(s[top-1]=='(') top--; //出栈 else return 0; break; case ']': if(s[top-1]=='[') top--; //出栈 else return 0; break; case '}': if(s[top-1]=='{') top--;//出栈 else return 0; break; } } if(top==0) return 1; //输出1表示匹配成功 else return 0; //输出0表示匹配失败 } int main() { char s[Max]; printf("请输入括号:"); scanf("%s",s); if(isValid(s) != 0) printf("true"); else printf("false"); return 0; }3.5【739】每日温度
方法一:从左到右依次遍历 O(n^2)
针对每一个温度值 向后进行依次搜索 ,找到比当前温度更高的值,这是最容易想到的办法。
其原理:从左到右除了最后一个数其余全部的数都遍历一次,最后一个数据对应的结果确定是 0,就不须要计算。
遍历的时候,每一个数都去向后数,直到找到比它大的数,数的次数就是对应输出的值。
#include <stdio.h> #include <stdlib.h> void dailyTemperatures(int* T, int TSize){ int i,j,result[TSize]; for(i = 0;i < TSize;i++) { int cur = T[i]; if(cur < 100) { for(j = i+1;j < TSize;j++) { if(T[j] > cur) { result[i] = j - i; break; } } if(j == TSize) result[i] = 0; } } for(i = 0;i < TSize;i++) { printf("%d\t",result[i]); } } int main() { int length,T[] = {73, 65, 85, 71, 69, 72, 76, 79}; length = sizeof(T) / sizeof(T[0]); dailyTemperatures(T,length); return 0; }
//LeetCode能够经过,但超时,由于时间复杂度为O(2^2) int* dailyTemperatures(int* T, int TSize,int* returnSize){ int i,j; int *result = malloc(sizeof(int)*TSize); //动态数组 *returnSize = TSize; for(i = 0;i < TSize;i++) { int cur = T[i]; if(cur <= 100) { for(j = i+1;j < TSize;j++) { if(T[j] > cur) { result[i] = j - i; break; } } //若以后再也不升高,则为0 if(j == TSize) result[i] = 0; } } return result; }方法二:从右到左依次遍历
关键是要减小为每一个数寻找值遍历次数。以下图所示,绿色部分区域会给屡次遍历,若是咱们能减小这部分区域的遍历次数,就能总体提升运算效率。
若是咱们先从计算右边,那么咱们计算过的位置就不须要重复计算,如图所示:
当前咱们须要计算 7575 位置,而后向右遍历到 7171,由于咱们已经计算好了 7171 位置对应的值为 22,那么咱们就能够直接跳 22 为在进行比较,利用了已经有的结果,减小了遍历的次数。
#include <stdio.h> #include <stdlib.h> void dailyTemperatures(int* T, int TSize) { int i,j; int *result = malloc(sizeof(int)*TSize); //动态数组 //从右向左遍历 result[TSize - 1] = 0; //最后一个必定为0 for(i = TSize - 2; i >= 0; i--) { // j+= result[j]是利用已经有的结果进行跳跃 for(j = i+1; j < TSize; j+=result[j]) { if(T[j] > T[i]) { result[i] = j - i; break; } //遇到0表示后面不会有更大的值,那固然当前值就应该也为0 else if(result[j] == 0) { result[i] = 0; break; } } } for(i = 0; i < TSize; i++) { printf("%d\t",result[i]); } } int main() { int length,T[] = {73, 65, 85, 71, 69, 72, 76, 79}; length = sizeof(T) / sizeof(T[0]); dailyTemperatures(T,length); return 0; }
方法三:堆栈 O(n)
能够运用一个堆栈 stack 来快速地知道须要通过多少天就能等到温度升高。从头至尾扫描一遍给定的数组 T,若是当天的温度比堆栈 stack 顶端所记录的那天温度还要高,那么就能获得结果。
- 对第一个温度23度,堆栈为空,把它的下标压入堆栈;
- 下一个温度24度,高于23度高,所以23度温度升高只需1天时间,把23度下标从堆栈里弹出,把24度下标压入;
- 一样,从24度只须要1天时间升高到25度;
- 21度低于25度,直接把21度下标压入堆栈;
- 19度低于21度,压入堆栈;
- 22度高于19度,从19度升温只需1天从 21 度升温须要 2 天;
- 因为堆栈里保存的是下标,能很快计算天数;
- 22 度低于 25 度,意味着还没有找到 25 度以后的升温,直接把 22 度下标压入堆栈顶端;
- 后面的温度与此同理。
该方法只须要对数组进行一次遍历,每一个元素最多被压入和弹出堆栈一次,算法复杂度是 O(n)。
利用堆栈,还能够解决以下常见问题:
- 求解算术表达式的结果(LeetCode 22四、22七、77二、770)
- 求解直方图里最大的矩形区域(LeetCode 84)