C语言实现Hash Map(2):Map代码实现详解

在上一节C语言实现Hash Map(1):Map基础知识入门中,我们介绍了Map的基础概念和在C++中的用法。但我写这两篇文章的目的是,能够在C语言中实现这样的一个数据结构,毕竟有时我们的项目中可能会用到Map,但是C语言库中并没有提供相关的数据结构供我们使用。所以这一节,我们就来看一下在C语言中是如何实现Map的。

  • 参考代码:https://github.com/rxi/map

文章目录

  • 1 使用实例
  • 2 代码分析
    • 2.1 map_init
      • 2.1.1 map相关数据结构
        • 2.1.1.1 变量声明
        • 2.1.1.2 map_t
        • 2.1.1.3 map_base_t
        • 2.1.1.4 map_node_t
      • 2.1.2 初始化函数
    • 2.2 通用函数分析
      • 2.2.1 map_hash:求哈希值
      • 2.2.2 map_newnode:创建新节点
      • 2.2.3 map_bucketidx:计算桶
        • 2.2.3.1 桶的数量和哈希值的关系
      • 2.2.4 map_addnode:添加节点
      • 2.2.5 map_resize:重新调整哈希表大小
    • 2.3 map_set:设置键值
        • 2.3.1 函数参数和值sizeof
        • 2.3.2 map_getref
        • 2.3.3 添加新节点
    • 2.4 map_get:获取键对应的值
    • 2.5 删除键值和遍历
  • 3 总结

1 使用实例

我们学习的过程一定是从已经成熟运用的代码中学习的,所以本文就来学习一下Github中这个已经被很多人用在项目中的map库。文件很简单,就一个map.cmap.h。我们拿到这个代码就可以直接使用的,非常简单:

#include <stdio.h>
#include <stdlib.h>
#include "map.h"

static map_str_t langMap;

int main()
{
    char *ret;
    map_init(&langMap);
    map_set(&langMap, "test", "1234");
    ret = map_get(&langMap, "test");
    if(ret != NULL)
    {
        printf("%s\r\n", ret);
    }else
    {
        printf("NULL\r\n");
    }
    return 0;
}

程序输出如下,可以看到我们初始化之后只需要设置键和值,然后使用map_get函数即可获取对应键的值了。

在这里插入图片描述

下面我们就来分析一下这里面的代码。

2 代码分析

现在,我们就基于我上面写的一个简单的例子,来分析一下代码完成了哪些操作。

2.1 map_init

2.1.1 map相关数据结构

2.1.1.1 变量声明

这里我声明了一个langMap变量:

static map_str_t langMap;

map.h文件中有声明不同的typedef:

typedef map_t(void*) map_void_t;
typedef map_t(char*) map_str_t;
typedef map_t(int) map_int_t;
typedef map_t(char) map_char_t;
typedef map_t(float) map_float_t;
typedef map_t(double) map_double_t;

这里键的类型固定是char *,上面我的例子中使用的是map_str_t这个typedef,实际上就是定义值的类型,也就是这里键和值都是char *。如果想要值是其他的类型,定义其它的类型就行了。

2.1.1.2 map_t

接下来看一下这个宏定义:

#define map_t(T)\
  struct { map_base_t base; T ref;}

可以看到就是根据用户提供的数据类型,声明一个对应的ref变量。

2.1.1.3 map_base_t

再来看一下map_base_t的数据结构,这实际上也是我们map的核心数据结构:

typedef struct {
  map_node_t **buckets;
  unsigned nbuckets, nnodes;
} map_base_t;

根据上一节我们学到的知识,通过nbucketsnnodes的名字,我们就可以猜测其含义如下:

  1. nbuckets
    • 作用:表示哈希表中桶(bucket)的数量。桶是哈希表的基本存储单元,每个桶可以包含零个或多个键值对(节点)。
    • 用途:nbuckets 用于确定将键值对分配到哪个桶中。哈希值经过处理后,取模操作决定具体的桶索引。
  2. nnodes
    • 作用:表示哈希表中当前存储的键值对(节点)的数量。
    • 用途:nnodes 用于跟踪哈希表中的实际元素数目。这个信息对于决定是否需要调整哈希表的大小(例如扩展或收缩)非常重要。当 nnodes 达到 nbuckets 的某个临界值时(如 nnodes 等于或超过 nbuckets),哈希表需要进行扩展以保持较低的碰撞率和较高的性能。
2.1.1.4 map_node_t

map.h中,声明了map_node_t

struct map_node_t;
typedef struct map_node_t map_node_t;

这个结构体的实例在map.c中:

struct map_node_t {
  unsigned hash;
  void *value;
  map_node_t *next;
  /* char key[]; */
  /* char value[]; */
};

这里的几个参数有什么作用,后面我们在代码中碰到了再分析。

2.1.2 初始化函数

这里的map_init函数实际上只是一个宏定义:

#define map_init(m)\
  memset(m, 0, sizeof(*(m)))

只是将map_str_t中各个数据结构清零,在有些RAM中,上电后初始值不一定为0,所以保险起见,还是清空一下。

2.2 通用函数分析

在分析设置键值函数之前,我们首先来学习一下后面可能会在函数中用到的一些通用的函数。

2.2.1 map_hash:求哈希值

map_hash 是一个用于计算字符串哈希值的函数。它采用了经典的 DJB2 哈希算法,这是一种快速且分布均匀的字符串哈希算法。以下是对 map_hash 函数的详细介绍:

static unsigned map_hash(const char *str) {
  unsigned hash = 5381;
  while (*str) {
    hash = ((hash << 5) + hash) ^ *str++;
  }
  return hash;
}

map_hash 函数利用 DJB2 哈希算法计算一个字符串的哈希值。DJB2 算法的核心思想是通过不断地乘以一个质数(在这里是33:左移5位+1)并进行异或操作来更新哈希值,以确保哈希值的分布均匀并减少冲突。

这个哈希函数在哈希表的实现中扮演着重要角色,因为它决定了键在哈希表中的存储位置。哈希值的质量直接影响哈希表的性能,包括查找、插入和删除操作的效率。

2.2.2 map_newnode:创建新节点

前面我们提到节点的数据结构是map_node_t,这个函数就是动态分配一个map_node_t节点并返回,实现如下:

static map_node_t *map_newnode(const char *key, void *value, int vsize) {
  map_node_t *node;
  int ksize = strlen(key) + 1;
  int voffset = ksize + ((sizeof(void*) - ksize) % sizeof(void*));
  node = MAP_MALLOC(sizeof(*node) + voffset + vsize);
  if (!node) return NULL;
  memcpy(node + 1, key, ksize);
  node->hash = map_hash(key);
  node->value = ((char*) (node + 1)) + voffset;
  memcpy(node->value, value, vsize);
  return node;
}

1、内存的分配和释放函数

大家可以移植单片机中的,比如有FreeRTOS,就可以移植vPortMallocvPortFree,我这里使用c库里的内存分配函数:

#define MAP_MALLOC malloc
#define MAP_FREE free

2、((sizeof(void*) - ksize) % sizeof(void*))

这很明显就是根据CPU的位数(sizeof(void *))来进行字节对齐。


再回来看一下map_node_t的数据结构:

struct map_node_t {
  unsigned hash;
  void *value;
  map_node_t *next;
  /* char key[]; */
  /* char value[]; */
};

这里内存分配的总大小是sizeof(*node) + voffset + vsize。其中,node为上面声明的map_node_t数据结构的总大小,然后voffset为键所占的字节对齐后的内存大小,value为值所占的内存大小。这里键和值的内存由于是不固定的,所以没有声明在结构体中,我们直接将键和值放在map_node_t的后面。

**如果后续匹配了,怎么获取键值?**获取键很容易,就在map_node_t最后,对于值的话,每次通过键设置或查值的时候,再计算一下voffset就行了。

2.2.3 map_bucketidx:计算桶

map_bucketidx 函数用于确定一个哈希值应该被放置到哈希表的哪个桶(bucket)中。很明显这个函数通过将哈希值与哈希表中的桶数量进行模运算来计算桶的索引。

  • 这里使用位与运算代替取模的话可以加快运算速度,但需要保证nbuckets的值是2n
static int map_bucketidx(map_base_t *m, unsigned hash) {
  /* If the implementation is changed to allow a non-power-of-2 bucket count,
   * the line below should be changed to use mod instead of AND */
  return hash & (m->nbuckets - 1);
}
2.2.3.1 桶的数量和哈希值的关系

在哈希表中,桶的数量(nbuckets)和哈希值之间的关系如下:

  • 哈希值:由 map_hash 函数计算得到,它是一个无符号整数,用于唯一标识一个键。
  • 桶的数量(nbuckets:表示哈希表中可用桶的数量。每个桶可以包含零个或多个键值对(节点)。
  • 桶索引:由 map_bucketidx 函数通过位与运算计算得到,用于决定哈希值被分配到哪个桶中。

2.2.4 map_addnode:添加节点

由前面的buckets的声明我们知道,buckets可以理解为map_node_t的指针的数组,数组中的每一个元素代表一个桶,每个桶也是map_node_t,里面有一个next参数,这类似于链表的数据结构,就可以连接当前桶内的所有节点。

static void map_addnode(map_base_t *m, map_node_t *node) {
  int n = map_bucketidx(m, node->hash);
  node->next = m->buckets[n];
  m->buckets[n] = node;
}

所以上面的函数就很好理解了,就是把新节点插入桶中链表的最前面。

2.2.5 map_resize:重新调整哈希表大小

map_resize 函数用于调整哈希表的大小(桶的数量)。当哈希表中的节点数超过一定比例时,通过增加桶的数量来减小冲突,提高查找、插入和删除操作的效率。具体来说,map_resize 函数将重新分配哈希表中的所有节点,使它们分布在新的桶中。下面是该函数的详细解释:

static int map_resize(map_base_t *m, int nbuckets) {
  map_node_t *nodes, *node, *next;
  map_node_t **buckets;
  int i;
  /* Chain all nodes together */
  nodes = NULL;
  i = m->nbuckets;
  while (i--) {
    node = (m->buckets)[i];
    while (node) {
      next = node->next;
      node->next = nodes;
      nodes = node;
      node = next;
    }
  }
  /* Reset buckets */
  buckets = realloc(m->buckets, sizeof(*m->buckets) * nbuckets);
  if (buckets != NULL) {
    m->buckets = buckets;
    m->nbuckets = nbuckets;
  }
  if (m->buckets) {
    memset(m->buckets, 0, sizeof(*m->buckets) * m->nbuckets);
    /* Re-add nodes to buckets */
    node = nodes;
    while (node) {
      next = node->next;
      map_addnode(m, node);
      node = next;
    }
  }
  /* Return error code if realloc() failed */
  return (buckets == NULL) ? -1 : 0;
}

简单分析一下上面的代码:

1、链表化所有节点

将所有节点串成一个单链表。遍历当前所有桶,将节点从桶中移除并加入到新的链表 nodes 中。

2、重新分配桶

使用 realloc 函数重新分配桶数组的内存,使其大小调整为新的桶数量 nbuckets。如果 realloc 成功,更新哈希表的桶指针和桶数量。

  • 注意:前面提到我们可以替换内存分配和释放的宏定义为自己的,但是这里又出现一个realloc函数,这个是在stdlib.h中的,在FreeRTOS中肯定是没有的,我们最好也不要用两种内存分配的方法,后面我们对这部分的代码做一些优化。

3、重新初始化桶

如果桶重新分配成功,则将新的桶数组初始化为 0,并将所有节点重新插入到新的桶中。通过 map_addnode 函数重新计算每个节点的桶索引,并将节点添加到对应的桶中。

2.3 map_set:设置键值

从前面的例子中,初始化之后就直接设置键值了:

map_set(&langMap, "test", "1234");

这也是这里map实现的核心,这就是一个简单的宏定义:

#define map_set(m, key, value)\
  ( map_set_(&(m)->base, key, value, sizeof(value)) )

我们主要来看一下map_set_是如何实现的:

int map_set_(map_base_t *m, const char *key, void *value, int vsize) {
  int n, err;
  map_node_t **next, *node;
  /* Find & replace existing node */
  next = map_getref(m, key);
  if (next) {
    memcpy((*next)->value, value, vsize);
    return 0;
  }
  /* Add new node */
  node = map_newnode(key, value, vsize);
  if (node == NULL) goto fail;
  if (m->nnodes >= m->nbuckets) {
    n = (m->nbuckets > 0) ? (m->nbuckets << 1) : 1;
    err = map_resize(m, n);
    if (err) goto fail;
  }
  map_addnode(m, node);
  m->nnodes++;
  return 0;
fail:
  if (node) MAP_FREE(node);
  return -1;
}
2.3.1 函数参数和值sizeof

先来看一下函数的参数,其中m就是map_base_t变量的地址,key就是键,value就是值的地址,vsize就是值的大小。现在这里有一个问题,我们使用sizeof(value)来获取值的长度,值的类型有以下几种:

typedef map_t(void*) map_void_t;
typedef map_t(char*) map_str_t;
typedef map_t(int) map_int_t;
typedef map_t(char) map_char_t;
typedef map_t(float) map_float_t;
typedef map_t(double) map_double_t;

对于void *char *int来说都没什么问题,分别返回4,字符串的长度(如果输入的是一个字符串常量的话)和4。但是:

1、char:如sizeof('c')

在C语言中,字符字面量(例如 'c')的类型是 int,而不是 char。因此,sizeof('c') 实际上会返回 sizeof(int) 的值,这通常是 4 字节(在大多数现代系统上)。这可能与期望的 sizeof(char) 返回值(通常为1字节)不同。

2、floatdouble

大家可以试一下,sizeof(3.14)sizeof(2.71828) 实际上都会返回 sizeof(double),因为在C语言中,字面值浮点数默认为 double 类型。

也就是说,这里的sizeof并不是实际的大小。


注意:在这个仓库的readme中,使用的是map_int_t类型举例的:map_set(&m, “testkey”, 123),这样明显也是不行的,因为第三个参数是void *,这里却直接传了一个数字。按照数据类型来看,这里还要声明一个int变量,然后map_set传地址才行,那这样完全变成了void *类型的了 基于此,使用map_str_t肯定是没有问题的,但是使用其它的几个数据类型,程序肯定有问题,要么编译不通过,要么通过了也内存越界,大家可以自己试一下。

也就是说,虽然这个map实现在github中是star比较多的,但是bug还是挺多的。我们有时可能还是希望可以直接设置值,而不是还要声明一个变量。所以本篇文章仅以map_str_t例子举例,实际产品用这个也是没问题的。


好了,我们暂时不纠结这个数据类型的问题,至少整个代码的map实现逻辑是没有问题的,只是兼容性这边出了点问题。下面我们开始分析map_set_函数。

2.3.2 map_getref

首先执行的是map_getref函数,下面是这个函数的实现:

static map_node_t **map_getref(map_base_t *m, const char *key)
{
  unsigned hash = map_hash(key);
  map_node_t **next;
  if (m->nbuckets > 0) {
    next = &m->buckets[map_bucketidx(m, hash)];
    while (*next) {
      if ((*next)->hash == hash && !strcmp((char*) (*next + 1), key)) {
        return next;
      }
      next = &(*next)->next;
    }
  }
  return NULL;
}

我们暂时不知道nbucketsbuckets数组在哪里设置的,还有它们的作用是什么。但是从这个函数大概可以知道,大概就是先求键的哈希值,然后去寻找一下是否有相同的键(有可能不同的键有同一个hash),如果有的话就返回这个节点指针的地址,没有的话就返回NULL。来看一下代码:

next = map_getref(m, key);
if (next) {
    memcpy((*next)->value, value, vsize);
    return 0;
}

如果该key的节点已经存在的话,就直接修改这个节点的值即可,函数直接返回。

2.3.3 添加新节点

继续分析map_set_中的代码:

    /* Add new node */
    node = map_newnode(key, value, vsize);
    if (node == NULL) goto fail;
    if (m->nnodes >= m->nbuckets) {
    n = (m->nbuckets > 0) ? (m->nbuckets << 1) : 1;
    err = map_resize(m, n);
    if (err) goto fail;
    }
    map_addnode(m, node);
    m->nnodes++;
    return 0;
fail:
    if (node) MAP_FREE(node);
    return -1;

简单分析一下:

1、节点不存在则创建节点

2、如果当前节点数量超过或等于桶的数量,计算新的桶数量**(这里设置为当前桶数量的两倍)**,然后调用 map_resize 函数调整哈希表大小。

  • 刚运行没初始化的时候,m->nbuckets设置为1

3、添加新节点到对应的桶中,并增加 nnodes 个数

  • 注意:从代码中可以看出桶的数量是我们设置节点的时候动态增加的,而且使用的是realloc函数,后续我们可以优化为上电初始化后默认有n个桶

2.4 map_get:获取键对应的值

在前面的示例代码中,设置完键值之后就可以使用map_get获取对应键的值了,返回值就是值的地址:

ret = map_get(&langMap, "test");

同样,这个函数也是一个宏定义:

#define map_get(m, key)\
  ( (m)->ref = map_get_(&(m)->base, key) )
  • 前面用宏定义map_t声明的不同数据类型的宏定义中的ref变量,只是用来临时保存值的,这个变量在其它地方都没有使用到。

所以我们就来看一下map_get_函数的实现:

void *map_get_(map_base_t *m, const char *key) {
  map_node_t **next = map_getref(m, key);
  return next ? (*next)->value : NULL;
}

前面分析过map_getref函数了:根据哈希值找到对应的桶,然后在桶中找匹配的哈希值,若哈希值匹配(有可能不同的键有同样的哈希值),再比较键,若匹配,返回键的值。

2.5 删除键值和遍历

代码中还提供了删除键值的函数map_remove,还有遍历map的函数map_itermap_next,实际上就是链表的一些操作,本文就不做分析了。

3 总结

基于本篇文章,我们已经学习到了哈希map实现的基本逻辑。另外,前面我们有提到,这个代码在值声明为其它几个数据类型的情况下,根本运行不了,或者并不方便我们开发程序(有时我们希望直接传值而不是变量地址),然后还有内存分配和初始化桶数量的地方可以优化。那么下一篇文章,我们就来解决这些问题,并优化这个代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/640636.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

springboot vue 开源 会员收银系统 (2) 搭建基础框架

前言 完整版演示 前面我们对会员系统https://blog.csdn.net/qq_35238367/article/details/126174288进行了分析 确定了技术选型 和基本的模块 下面我们将从 springboot脚手架开发一套收银系统 使用脚手架的好处 不用编写基础的rabc权限系统将工作量回归业务本身生成代码 便于…

【通义千问—Qwen-Agent系列2】案例分析(图像理解图文生成Agent||多模态助手|| 基于ReAct范式的数据分析Agent)

目录 前言一、快速开始1-1、介绍1-2、安装1-3、开发你自己的Agent 二、基于Qwen-Agent的案例分析2-0、环境安装2-1、图像理解&文本生成Agent2-2、 基于ReAct范式的数据分析Agent2-3、 多模态助手 附录1、agent源码2、router源码 总结 前言 Qwen-Agent是一个开发框架。开发…

【LeetCode】【209】长度最小的子数组(1488字)

文章目录 [toc]题目描述样例输入输出与解释样例1样例2样例3 提示进阶Python实现前缀和二分查找滑动窗口 个人主页&#xff1a;丷从心 系列专栏&#xff1a;LeetCode 刷题指南&#xff1a;LeetCode刷题指南 题目描述 给定一个含有n个正整数的数组和一个正整数target找出该数组…

Java进阶学习笔记3——static修饰成员方法

成员方法的分类&#xff1a; 类方法&#xff1a;有static修饰的成员方法&#xff0c;属于类&#xff1a; 成员方法&#xff1a;无static修饰的成员方法&#xff0c;属于对象。 Student类&#xff1a; package cn.ensource.d2_staticmethod;public class Student {double scor…

SpringMVC流程

1、SpringMVC常用组件&#xff1a; DispatcherServlet&#xff08;请求分发器&#xff09;&#xff1a;Spring MVC的核心组件之一&#xff0c;负责处理全局配置和将用户请求分发给其他组件进行处理。Controller&#xff08;处理器&#xff09;&#xff1a; 实际处理业务逻辑的…

springmvc中HandlerMapping是干什么用的

HandlerMapping处理器映射器 作用是根据request找到相应的处理器Handler和Interceptors&#xff0c;然后封装成HandlerExecutionChain对象返回 HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; 实现类 HandlerMapping帮助DispatcherServlet进…

Oblivion Desktop:一款强大的网络工具介绍

一款优秀的开源网络工具。 文章目录 Oblivion Desktop: 安全与隐私的网络工具软件背景开发背景 使用方法安装日常使用高级功能 总结 Oblivion Desktop: 安全与隐私的网络工具 软件背景 Oblivion Desktop 是一个由 BePass 团队开发的开源桌面应用&#xff0c;旨在为用户提供更…

喜报 | 江苏刺掌信息科技有限公司获选市企业发展服务中心优质合作伙伴

喜报 江苏刺章信息成功入选 镇江市企业发展服务中心 “优质合作伙伴” 为进一步完善镇江市公共服务体系建设&#xff0c;提升服务范围和能力&#xff0c;更好地为企业提供专业、高效、安全的服务&#xff0c;镇江市企业发展服务中心启动了优质合作伙伴的征选工作&#xff0c;通…

win10右键没有默认打开方式的选项的处理方法

问题描述 搞了几个PDF书籍学习一下&#xff0c;不过我不想用默认的WPS打开&#xff0c;因为WPS太恶心人了&#xff0c;占用资源又高。我下载了个Sumatra PDF&#xff0c;这时候我像更改pdf文件默认的打开程序&#xff0c;发现右击没有这个选项。 问题解决 右击文件–属性–…

Linux——进程与线程

进程与线程 前言一、Linux线程概念线程的优点线程的缺点线程异常线程用途 二、Linux进程VS线程进程和线程 三、Linux线程控制创建线程线程ID及进程地址空间布局线程终止线程等待分离线程 四、习题巩固请简述什么是LWP请简述LWP与pthread_create创建的线程之间的关系简述轻量级进…

JAVA云HIS医院系统源码 HIS源码:云HIS系统与SaaS的关系

云HIS系统与SaaS的关系 云HIS系统是一种基于云计算技术的医院信息系统&#xff0c;它采用B/S架构&#xff0c;通过云端SaaS服务的方式提供。用户可以通过浏览器访问云HIS系统&#xff0c;无需关注系统的部署、维护、升级等问题。云HIS系统通常具有模板化、配置化、智能化等特点…

Android 共享内存

Parcelable 和 Serializable 区别 Serializable IO完成&#xff08;通过磁盘文件读写&#xff09; Parcelable C 对象指针 来实现共享内存 import android.os.Parcel; import androidx.annotation.NonNull;public class ApiResponseBean extends Throwable implements Parce…

吴恩达2022机器学习专项课程C2W2实验:Relu激活函数

目录 代码修改1.Activation2.Dense3.代码顺序 新的内容1.总结上节课内容2.展示ReLU激活函数的好处3.结论 代码案例一代码案例二1.构建数据集2.构建模型 2D1.构建数据集2.模型预测3.扩展 代码修改 1.Activation &#xff08;1&#xff09;需要添加代码from tensorflow.keras i…

深度学习模型keras第二十四讲:KerasNPL概述

1、KerasNPL简介 KerasNLP是一个与TensorFlow深度集成的库&#xff0c;旨在简化NLP&#xff08;自然语言处理&#xff09;任务的建模过程。它提供了一系列高级API&#xff0c;用于预处理文本数据、构建序列模型和执行常见的NLP任务&#xff0c;如情感分析、命名实体识别和机器…

深入解析力扣162题:寻找峰值(线性扫描与二分查找详解)

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

php基础笔记

开端&#xff1a; PHP 脚本可以放在文本的任意位置 PHP 脚本以 开始&#xff0c;以 ?>** 结束&#xff1a; PHP 文件的默认文件扩展名是 ".php" 标签替换 <? echo 123;?> //short_open_tagson 默认开启 <?(表达式)?> 等价于 <?php echo …

virtual box ubuntu20 全屏展示

virtual box 虚拟机 ubuntu20 系统 全屏展示 ubuntu20.04 视图-自动调整窗口大小 视图-自动调整显示尺寸 系统黑屏解决 ##设备-安装增强功能 ##进入终端 ##终端打不开&#xff0c;解决方案-传送门ubuntu Open in Terminal打不开终端解决方案-CSDN博客 ##点击cd盘按钮进入文…

保存商品信息功能(VO)

文章目录 1.分析前端保存商品发布信息的json数据1.分析commoditylaunch.vue的submitSkus1.将后面的都注销&#xff0c;只保留查看数据的部分2.填写基本信息3.保存信息&#xff0c;得到json4.使用工具格式化一下 2.使用工具将json转为model3.根据业务修改vo&#xff0c;放到vo包…

力扣hot100学习记录(七)

240. 搜索二维矩阵 II 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 题意 在二维矩阵中搜索是否存在一个目标值&#xff0c;该矩阵每一行每一列都是升序…

❤ Vscode和Idea都可以使用的-AI插件(官方-百度出的)

❤ Vscode和Idea都可以使用的-AI插件&#xff08;官方-百度出的&#xff09; 最新AI特别火&#xff0c;给大家推荐一下最新出的VScode插件&#xff0c;辅助我们写代码&#xff01; 1、下载地址&#xff1a; > https://comate.baidu.com/zh/shopping?inviteCodefkzlak8f …