代码随想录算法训练营第六天 | 242.有效的字母异位词、349. 两个数组的交集、202. 快乐数、1. 两数之和

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++的执行过程

  1. *str:解引用指针str,获取指针所指向的值(即字符串的首地址)。
  2. str++:将指针str向后移动一个位置(C语言会自动判断移动几个字节),指向下一个指针元素。
  3. 总结 - *str++中的自增操作作用于指针str,而不是(*str)。
  4. 过程: 先取值,后对值进行++ <===> *(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;
}
  • 正确的运行结果

正确结果

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值