day_06
传送门
1. 哈希表基础
-
基础速看 - hash基础
-
什么是哈希表
- 是一种映射并存储数据的数据结构
- <k , v> 一个 k 只对应一个内存地址,内存地址里存放 v。找到 k,就能找到 v
- k,v 的数据类型支持自定义
-
什么是数据结构
- 存储( 链式 + 非链式 ) + 操作( 增删改查 ) 数据的特定结构
- 线性与否是更具数据需要的内存地址来体现的,需要连续内存的就是线性,不需要的就是非线性
- 根据上一条,可知链式就是非线性,非链式就是线性,
-
常见的数据结构有哪些
- 数组
- 链表
- 栈
- 队列
- 树
- 图
- 哈希表
-
数组(Array):线性存储结构,元素按照连续的内存地址存储,通过索引访问和操作元素。 链表(Linked List):使用指针连接节点的数据结构,每个节点存(数据 + 指向下一个节点的指针)。 栈(Stack):一种后进先出(LIFO)的数据结构,只允许在栈顶进行插入和删除操作。 队列(Queue):一种先进先出(FIFO)的数据结构,通过队尾插入元素、队头删除元素。 树(Tree):一种非线性的数据结构,由节点和边组成,每个节点可以有多个子节点。 图(Graph):一种具有节点和边的非线性数据结构,用于表示事物之间的关系。 哈希表(Hashtable):一种利用哈希函数来快速存储和查找数据的数据结构
- hash表分类
-
set - 一个数只能在该集合出现一次,可以保证值唯一
- treeSet 有序Set集合,红黑树实现,可以指定排序规则,自定义类型一定要指定排序规则
- hashset 无序set集合,顺序与插入顺序不同
- LinkedSet 与元素插入顺序相同的set集合
-
map - <k,v> 只有 k 是唯一的,v可以重复,与上面类似
- treemap
- hashMap
- LinkedMap
-
数组 - 最常见的 hash 表,可以直接自定义
- 数组也是映射关系,符合 hash表的定义
- 缺点:数组的大小可没办法无限开辟的
-
- 常见的hash表具体实现
-
拉链法
-
线性探测比较无聊,在此不再细说
-
242.有效的字母异位词
- 思路 :
典型的快速判断一个元素有没有出现在集合里
1. 所以遍历第一个字符串,并存到 hash 表里面(map, set, array)三选一,如果存的是唯一值选set,存的值不唯一或者要时常改变,选其他两个
2. k存字母,v存出现次数,维护好这个hash表
3. 遍历第二个字符串,hash表里如果出现过,就给k对应的 v值减去 1
4. 看表里的值是不是都为 0, T:两个是, F:两个不是
- 代码实现
- java实现 - 可以使用java现成的 hashMap 处理
class Solution { public boolean isAnagram(String s, String t) { HashMap<Character,Integer> hashMap = new HashMap<>(); for (Character ch : s.toCharArray()) { // 遍历 s,构造 hash表 if (hashMap.containsKey(ch)){ // 存在该字母,就给字母+1 Integer curCount = hashMap.get(ch); hashMap.put(ch, curCount + 1); } else { hashMap.put(ch, 1); } } for (Character ch : t.toCharArray()) { // 遍历 t,看看表里是不是都有 if(!hashMap.containsKey(ch)){ // 如果表里不包含该字符,说明肯定不相等了 return false; } int curCount = hashMap.get(ch); // 反之包含,就更新表 hashMap.put(ch,curCount - 1); } // foreach循环遍历 hashmap for (Map.Entry<Character,Integer> cur : hashMap.entrySet()){ if (cur.getValue() != 0){ // 如果有 != 0 的表数据,说明不匹配 return false; } } return true; } }
- C语言实现
- 插入一个小问题,这里判断完后,字符串s、t的首地址变了吗?答案见后续的问题记录
bool isAnagram(char * s, char * t){ int letter[26]; memset(letter, 0, sizeof(letter)); // 将数组里的数字都置为0 int index; bool ans = true; // 开始遍历 s,关于为什么 *s 可以直接判断为假值,请见下方问题记录 while(*s){ index = *s - 'a'; letter[index]++; s++; } // 开始遍历 t while(*t){ index = *t - 'a'; letter[index]--; t++; } // 看看hash表里的记录 for(int i=0; i<26; i++){ if(letter[i] != 0) return false; } return abs; }
349 - 两个数组的交集
-
思路
- 遍历一个数组,存到 set-A 里面,因为这个题要的是集合的交集
- 遍历另一个数组,发现这个值在 set-A 里有的话,存到 set-B
- 最后返回 set-B 里的值
-
java实现
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set_a = new HashSet<>();
Set<Integer> set_b = new HashSet<>();
for (int cur : nums1) { // 遍历 nums1
if (!set_a.contains(cur)) set_a.add(cur); //不存在就填到里面
else continue; // 存在就跳过
}
for (int cur : nums2){ // 遍历 nums2
if (set_a.contains(cur)){ set_b.add(cur); } // a数组里有这个数
else continue;
}
int size = set_b.size();
int ans[] = new int[size];
int index = 0;
for (int cur : set_b) {
ans[index++] = cur;
}
return ans;
}
}
(ps.jdk8新特性,有更优雅的流写法,可以方便的转为数组)
(set_b.stream().mapToInt(x -> x).toArray()😉
202 - 快乐数
-
思路
- 每个位数单独取出,平方后求总和
- 如果等于 1,说明是快乐数,否则会进入无线循环
- 如果 set 集合里存在当前的总和,且 != 1,说明不是
-
java实现代码
class Solution {
public static boolean isHappy(int n) {
Set<Integer> hashSet = new HashSet<>();
hashSet.add(1); // 添加 1 到 set里面
while (true){ // 开始处理
n = getThisSum(n);
if (hashSet.contains(n)){ // set里有该数字时,退出循环
break;
}
hashSet.add(n);
}
return n == 1;
}
/**
* 循环取出每一位数字,取余,除10
* 1 % 10 = 1
* 1 / 10 = 0
* @param n 输入一个数
* @return 每一位数的平方,并对其求和
*/
public static Integer getThisSum(Integer n){
Integer minLocal = 0; // 最低位数据
Integer sum = 0;
while (n != 0){
minLocal = n % 10; // 得到最低为
sum += minLocal * minLocal; // 开始计数
n = n / 10; // 开始取前一位
}
return sum;
}
}
Q1. 两数之和
-
思路
- 双重遍历,第一个指针逐个++,另一个从头到尾扫描,目的是为了寻找到 +a后 =target的数
- 是否可以进行优化?
- 很明显,第二个指针是为了寻找数组里满足条件且已存在的数
- 快速查找 - 可以考虑 hash 表来处理这种情况!
- 选什么样的 hash表?
- 三种hash表可选:数组、map、set,这个题里会有 [3,3,3] 这样的不唯一值,故选择 map
- 如何选 key? val?
- key要唯一,题里要求返回数组里的下标值,一般来说,要求返回什么值,就选则该值作为 val
- v 就选下标
- k 就选当前的数字
- 遇到同一个 k 的时候,v 会被覆盖,但也不用担心,已经明确答案是唯一的,说明就算有同样的值,只有两个,肯定分布在一前一后
-
实现过程
- 待查值 find = target - nums[i]
- hash表需要遍历数组后构建, <数字,下标>
- 开始寻找 find
- 如果 find = nums[i], 要求 nums[i]下标 >= find
- 反之如果 find != nums[i], z找到后,就直接返回即可
-
代码实现
class Solution {
public static int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> hashMap = new HashMap<>();
int find; // 需要在表里查询的数字 find = target = nums[i]
int ans[] = new int[]{-1 , -1}; // 爪哇建数组真的挺麻烦的, new 相当于去申请了一个指针(地址)
int i; // 用来循环的数
// 构造 hash表
for (i = 0; i < nums.length; i++) {
hashMap.put(nums[i],i); // 建表,同一数字剩余的就是最后的下标
}
// 开始遍历 nums[]
for (i = 0; i < nums.length; i++) { // 包含数,且a,b的下标不相等,说明找到了!
find = target - nums[i];
if (hashMap.containsKey(find) && hashMap.get(find)!=i){
ans[0] = i;
ans[1] = hashMap.get(find);
// break; 找到就可以返回了,提交的时候没加这句
}
}
if (ans[0] == -1 ) return null;
return ans;
}
}
-
代码优化思路,走了两次循环还是太慢,能不能只走一次?
- 已经知道当数组里每个相同值的个数最多只有两个,且分布在一前一后
- 可以一边遍历一边填表查询
- 谁先谁后?
- 上面的代码实现中,知道相同后值的下标,但不知道前值,所以是通过前值找后值,前值通过遍历寻得
- 我们想一次遍历,当前遍历到的数字是否要先存到表里? 答案是 - NO
- 通过当前遍历到数值,直接查表,看看有咩有想要的值,相当于是知道后值找前值
-
二者区别如下
-
优化后代码实现
class Solution {
public static int[] twoSum(int[] nums, int target){
HashMap<Integer, Integer> hashMap = new HashMap<>();
int find = 0;
int ans[] = new int[]{-1 , -1};
for (int i = 0; i < nums.length; i++) {
find = target - nums[i];
if (hashMap.containsKey(find)){
ans[0] = i;
ans[1] = hashMap.get(find);
break;
}
hashMap.put(nums[i], i);
}
if (ans[0] == -1 ) return null;
return ans;
}
}
问题记录
1. C语言中的 memset
#include <stdio.h>
#include <string.h>
int main() {
printf("Hello, World! \n");
int letter[26];
memset(letter, 5, sizeof(letter));
for (int i = 0; i < 26; i++) {
printf("%d ", letter[i]);
}
return 0;
}
运行结果如下
很明显,错了,我想要每个数组里的值为 5 的
查阅后发现,memset是为每个字节赋值,int四个字节,
每个字节的值都为 5,所以输出了这样奇怪的结果
多字节的数组或结构体只能赋值0,-1(结果都为-1),Oxff(一个字节的最大值,这个值可能会有点不对,请自行保证是一个字节的最大值)
char类型的数组可以直接赋值,因为char只占一个字节
2. C语言中的 while() 假值判断
while(0) <======> while('\0')
这是因为 C语言中,对假值的定义为 : 目标值的机器码都为 0
所以就有:
while(*str) <======> while(*str != '\0')
ps. <========> 意为等价
3. C语言中的字符串
char[] 可以修改
*指向 char[] 可以修改,这是由于char[]是在栈区定义的数,栈区的值可以修改
*str = 'qqqq' 只读,无法修改
这是因为这个字符串在常量区,常量区的值无法修改
C语言里的字符串要求末尾一定是 '\0'作为结尾终止符
手动给 char[] 一个个的赋值时,需要手动添加末尾的终止符!!!
char str3[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 方式三
自动识别型就不用了
char *str1 = {"Hello world!"}; // 方式一 (可省略{})
char str2[] = {"Hello world!"}; // 方式二 (可省略{})
4. 关于C语言中字符串的遍历 - 遍历方法
- 二级指针
- char* str_arr[ ] = {“Monday”, “Tuesday”, NULL};
- 上面表示str_arr[ ]数组里存储的是char* ,故str_arr[]是指针数组
- C语言里,字符串常量会先被隐式的转为字符数组
- 隐式转为数组后,数组首地址又被转为 char* 指针
- 即[ “aaa” , “bbb” , “ccc” ] -> [arr1 , arr2 , arr3 ] -> [char* 1 , char* 2, char* 3]
- char** 二级指针的理解: [ (char*)* ] 一个指向了char*的指针(指针实际就是地址)
- 反过来,*str 就是一个 char* 数据
- str_arr是地址,地址里存的是一个char* 的地址值
- C语言里的左值其实已经隐式的解引用地址啦!
- 先取到 str_arr地址的值,即 *(str_arr) 得到char*,这个char* 就是 “aaa” 的首地址
- 再对 char* 解引用就可以得到字符串,printf(“%s”,*str++);
5. *str++的执行过程
- *str:解引用指针str,获取指针所指向的值(即字符串的首地址)。
- str++:将指针str向后移动一个位置(C语言会自动判断移动几个字节),指向下一个指针元素。
- 总结 - *str++中的自增操作作用于指针str,而不是(*str)。
- 过程: 先取值,后对值进行++ <===> *(str++),* 的优先级更高
6. 回答上面的 s、t指针是否会改变的问题
- 不会
- 因为C语言传参是按值传递的,所以传递的是原首地址的拷贝
- 想要修改怎么办?
- 判断完后,返回数组首地址,更新原先的首地址
- 传值不行,那就传地址,直接修改地址值里的数据
- 如何传地址?
直接传 char* 数组的地址,参数会自动拷贝一份
如果我们传的是 char** 呢,让原来的地址 (char* 数组地址)被一个指针指向,
传参的时候,传的就是 (char*)* 的拷贝了,原先的 (char* 数组地址)还在,
每次修改的时候,就能修改原数组的首地址了
小疑问 - 如何得到指向 char* 的指针呢?
得到指向 char* 的指针,也就是得到存储 char* 的地址
- 如何得到地址呢?已经知道了值为char* 的变量
对char* 使用&(取地址符号)就可以得到 **char的地址啦
- 传地址的实现代码如下
// ================= 这是一个错误样例 ==============
#include <stdio.h>
void movePoint(char **str){
int n = 5;
char* cur = *str; // 取到数组首地址, 但是 str里面的值没有改变
while( n-- ){
printf("%c",*cur);
cur++;
}
printf("\n");
}
int main()
{
char ch[] = "smile man, good will coming"; // 指定一个数组
char* str = ch; // 数组变量名指的是数组的首地址,str现在指向数组首地址
char** putin = &str;
movePoint(putin);
while( *str ){
printf("%c",*str);
str++;
}
return 0;
}
- 错误运行结果
- 正确样例(比较推荐,因为更加明了 ps.个人觉得)
#include <stdio.h>
void movePoint(char **str){
int n = 5;
char* cur = *str; // str解引用,取出数组首地址
while( n-- ){
printf("%c",*cur);
cur++;
*str = cur;
}
printf("\n");
}
int main()
{
char ch[] = "smile-man, good will coming"; // 指定一个数组
char* str = ch; // 数组变量名指的是数组的首地址,str现在指向数组首地址
char** putin = &str;
movePoint(putin);
while( *str ){
printf("%c",*str);
str++;
}
return 0;
}
- 更精简一些
#include <stdio.h>
void movePoint(char **str){
int n = 5;
while( n-- ){
printf("%c",**str);
// 修改 str地址里的值,*str是地址里的值,char* ,也就是数组的下标,
// 对 char* ++,就可以移动和查询并行啦
(*str)++;
// 如果用*str++,str指针++会指向野指针,所以不能使用
}
printf("\n");
}
int main()
{
char ch[] = "smile-man, good will coming"; // 指定一个数组
char* str = ch; // 数组变量名指的是数组的首地址,str现在指向数组首地址
char** putin = &str;
movePoint(putin);
while( *str ){
printf("%c",*str);
str++;
}
return 0;
}
- 正确的运行结果