Linux 本地用户和组管理

1. 概述

Linux 系统中的用户和组管理是权限控制的基础。它允许系统管理员定义谁可以访问哪些资源以及他们可以执行哪些操作。

2. 用户和组管理命令

2.1 用户管理命令

  • useradd: 添加用户
  • userdel: 删除用户
  • usermod: 修改用户属性

2.2 组管理命令

  • groupadd: 添加组
  • groupdel: 删除组

2.3 权限切换命令

  • su - username: 切换到指定用户(- 表示切换到该用户的环境)。
  • sudo command: 以超级用户权限执行命令。
  • visudo: 编辑 /etc/sudoers 文件,用于配置 sudo 权限。

3. 用户和组相关配置文件

  • /etc/passwd: 用户账户信息
  • /etc/group: 组信息
  • /etc/gshadow: 组密码和组管理员信息
  • /etc/sudoers: 配置哪些用户或组可以使用 sudo 以及他们可以执行哪些命令。

4. 实操演示

在Linux系统中,配置用户和组(例如使用 useradd, groupadd, userdel, groupdel, usermod 等命令)通常需要 root 用户的权限。这是因为用户和组的管理涉及到系统级别的安全和资源分配,只有 root 用户(或通过 sudo 获得 root 权限的用户)才能执行这些操作,以确保系统的稳定性和安全性。

4.1 切换到 Root 用户

首先我们使用su -来切换到root。

VirtualBoxVM_pxqixFiB5Q.png

4.2 添加用户

然后我们可以尝试使用useradd来添加一个账户。

VirtualBoxVM_FQLiI4LrjL.png

可以看到我们的账户已经创建完毕了,并且我们的/home目录下也多了一个目录。

4.3 创建组

接下来我们创建一个组试试。

VirtualBoxVM_B1mVSA6bGw.png

VirtualBoxVM_QNr99TvNze.png

/etc/group里面可以看到所有关于组和其id的信息。

4.4 删除用户

如果我们要删除用户的话,如果仅仅使用userdel noob,noob的home目录以及mailbox file仍然会存在,建议使用-r选项,在删除用户的同时,也移除该用户的家目录和邮箱文件。

VirtualBoxVM_ukurkiXySM.png

4.5 修改用户属性 (usermod)

然后我们尝试一下使用usermod。

VirtualBoxVM_xEu4SD6qtp.png

usermod -G用于修改一个用户所属的“附加组”,在 Linux 中,一个用户必须属于一个“主组” (primary group),并且可以同时属于多个“附加组”。

但是你有没有发现,我们执行完之后,/home目录下的noob目录仍然属于noob组。因为noob的主组还是noob。

如果你这时候使用usermod -g这个小写的g,它也只会改变用户在系统中的“默认主组”设置(即 /etc/passwd 文件中的 GID)。它不会自动去修改该用户已经拥有的所有文件(比如 /home/noob 目录)的属组。

4.6 修改文件属组 (chgrp)

如果你修改隶属于noob的所有文件变为fools所属的话,我们就需要使用change group命令chgrp

VirtualBoxVM_JQAyISr3ei.png

chgrp -R 中的 -R 选项代表 “Recursive”(递归)。它的意思是:不仅改变你指定目录的属组,还要进入这个目录,改变里面所有文件、所有子目录、以及子目录里的所有文件的属组,一直重复下去,直到覆盖所有内容。

4.7 深入理解配置文件

现在我们来看一看刚才提到的/etc/passwd有什么内容:

VirtualBoxVM_poiJUg52KB.png

可以看到每当你创建一个新用户的时候,最下方就会被添加新的信息。

/etc/passwd 是一个用冒号分隔的文本文件,其中每一行都代表一个系统用户账户。它定义了用户的核心信息,如用户名(第1字段)、用户ID (UID)(第3字段)、主组ID (GID)(第4字段)。它还指定了用户的家目录(第6字段)和登录时运行的 Shell 程序(第7字段),其中 /usr/sbin/nologin 表示该账户禁止登录。

我们继续看一看其他的文件都是什么内容,对于/etc/group

VirtualBoxVM_VHeuCP5Yu9.png

/etc/group 文件中的每一行都定义了一个用户组,它通过冒号分隔,依次显示了组名、占位符 (x)、组ID (GID)、以及该组的附加成员列表(用逗号分隔)。这个文件是 Linux 权限系统的核心,它通过 GID 来标识组,并通过附加成员列表来决定哪些用户可以“共享”这个组的权限。

/etc/group 文件的上下文中,那个 x 占位符的意思是:“这个组的加密密码存储在别处。”

这个“别处”就是 /etc/gshadow 文件。

  1. 历史原因: 在非常古老的 Unix/Linux 系统中,组的加密密码(如果设置了的话)会直接存放在 /etc/group 文件的第二个字段。
  2. 安全问题: /etc/group 文件需要被系统上所有用户读取(以便知道谁在哪个组里)。如果密码也存在这里,任何用户都可以拿到所有组的加密密码去尝试破解。
  3. 现代方案: 为了安全,系统把真正的组密码(如果有的话)转移到了一个只有 root 用户才能读取的 /etc/gshadow 文件中。原来的位置就用一个 x 来“占位”,表示“密码已启用并已转移”。

一句话总结: 那个 x 是一个安全改进的产物,它告诉你这个组的真实密码信息被安全地存放在 /etc/gshadow 文件里了。

(P.S. 实际上,“组密码”这个功能本身现在已经非常非常少用了。大多数组(如 x 所示)虽然启用了 shadow 密码,但并没有设置实际的密码。)

接下来我们来看看/etc/gshadow

VirtualBoxVM_gAqw7fhMRa.png

/etc/gshadow 文件是 /etc/group 文件的“影子文件”,它以安全的方式存储组密码组管理员信息,并且为了安全,它只能被 root 用户读取

它的每一行也由冒号 : 分隔,包含 4 个字段:

  1. 组名 (Group Name)

    • 对应于 /etc/group 中的组名。
  2. 加密的组密码 (Encrypted Password)

    • 这才是真正存储组密码的地方(而不是 /etc/group 里的 x)。
    • 如果这里是 !*,意味着这个组被锁定了,或者没有设置密码(这是最常见的情况)。
    • 如果这里是空的,表示该组没有密码。
  3. 组管理员 (Group Administrators)

    • 一个用逗号分隔的用户名列表,这些人可以管理这个组的成员(比如添加或删除成员)。
    • 这个字段通常是空的
  4. 组成员 (Group Members)

    • 一个用逗号分隔的用户名列表,这些人是这个组的成员。
    • 这和 /etc/group 文件的第 4 字段(附加成员列表)功能基本一样。

4.8 组合命令示例

接下来我们尝试一下组合命令:

VirtualBoxVM_D6DYVhjxvN.png

useradd -g fools -s /bin/bash -c "second noob" -m -d /home/noob2 noob2

  • useradd: “添加用户”的主命令。

  • -g fools:

    • -g (group) 指定用户的**“主组”**。
    • 强制将新用户 noob2 的主组设为 fools 组。
    • (后面的 id noob2 命令也确认了这一点,显示 gid=1002(fools))
  • -s /bin/bash:

    • -s (shell) 指定用户的**“登录 Shell”**。
    • noob2 的 Shell 设为 /bin/bash,这意味着该用户可以正常登录和使用命令行。
  • -c "second noob":

    • -c (comment) 添加**“备注”**信息,通常是用户的全名。
    • 备注内容是 “second noob”。
    • (后面的 tac /etc/passwd 命令确认了这一点)
  • -m:

    • -m (create home) 这是“创建家目录”的选项。
    • 它告诉 useradd 命令:“如果家目录不存在,就必须创建它。”
  • -d /home/noob2:

    • -d (directory) 指定**“家目录的路径”**。
    • noob2 的家目录路径设为 /home/noob2
  • noob2:

    • 放在命令最后的是你要创建的用户名

-m-d 是如何协同工作的?

这两个选项一起使用是标准操作:

  1. -d /home/noob2 告诉系统:“这个用户的家目录路径是 /home/noob2。”
  2. -m 告诉系统:“现在,请真的去创建 /home/noob2 这个目录。”

如果没有 -m 会怎样?
系统只会在 /etc/passwd 文件里记录家目录是 /home/noob2,但不会真的在磁盘上创建这个文件夹。当 noob2 登录时就会因为找不到家目录而出错。

Linux 文本编辑器

一、vi/vim 简介

文本编辑器是一个能够让您在Linux文件中创建和操作文本数据的程序。vi 是一个强大的可视化编辑器,几乎所有Linux发行版都内置,而 vim 是其功能更全面的增强版。

除了 vi/vim,Linux中还有其他文本编辑器,例如:

  • ed: 标准行编辑器
  • ex: 扩展行编辑器
  • emacs: 全屏编辑器
  • pico: 初学者编辑器

二、基本操作入门

1. 创建文件并进入 vi

首先,使用 vi 命令加上一个文件名来创建或打开文件。例如 vi myfile
VirtualBoxVM_x6PHLmwQd0.png

按下回车后,您将进入 vi普通模式
VirtualBoxVM_ckyRSkSg8s.png

此时左下角显示新文件名 myfile_file[New],表明您正处于普通模式下。

2. 插入模式:输入文本

在普通模式下,按下 i 键,即可进入插入模式
VirtualBoxVM_FWOWlMC0nh.png

左下角出现 -- INSERT --,表示您现在可以输入文本了。

现在,输入一些示例文字。
VirtualBoxVM_8MG3O4dQik.png

3. 保存与退出

输入完成后,需要先回到普通模式才能进行保存。
按下 Esc 键,左下角的 -- INSERT -- 消失,返回普通模式。
VirtualBoxVM_n2RuJesUL2.png

您有两种常用的方式来保存并退出:

方式一:命令模式

  1. 键入冒号 : 进入命令模式
  2. 输入 wq (w代表写入,q代表退出) 后按回车。

VirtualBoxVM_0ho7TE31XQ.png

方式二:快捷键
在普通模式下,连续按两次大写的 Z (即 Shift + z 两次),同样可以保存并退出。

回到命令行后,可以使用 cat myfile 命令查看文件内容,确认已保存。
VirtualBoxVM_MAywsgnCki.png

三、常用命令参考

1. 命令模式 (按 : 进入)

命令 功能
:w 保存文件 (Write)
:q 退出编辑器 (Quit)
:wq 保存并退出
:q! 强制退出,不保存任何修改
:w new_filename 将当前内容另存为 new_filename
:set nu 显示行号
:set nonu 隐藏行号

2. 普通模式 - 编辑操作

vi/vim 中,“复制” 称为 “Yank” (y),“删除” 称为 “Delete” (d),“粘贴” 称为 “Put” (p)。

删除

  • x: 删除当前光标所在的一个字符
  • dw: 删除从当前光标到下一个单词开头的内容 (Delete Word)。
  • d$: 删除从当前光标到行尾的内容。
  • dd: 删除当前整行 (被删除的内容会被自动复制)。
  • 5dd: 一次性删除 5 行。

复制

  • yw: 复制一个单词 (Yank Word)。
  • yy: 复制当前整行 (Yank Line)。
  • 5yy: 一次性复制 5 行。

粘贴

  • p: 在光标之后粘贴 (Put)。
  • P: 在光标之前粘贴。

撤销/重做/替换

  • u: 撤销上一步操作 (Undo)。
  • Ctrl + r: 重做上一步被撤销的操作 (Redo)。
  • r: 替换光标所在的一个字符 (按 r 后再按你想替换的字符)。

3. 普通模式 - 搜索操作

  • /search_term: 向下搜索 “search_term”。
  • ?search_term: 向上搜索 “search_term”。
  • n: 跳到下一个搜索结果 (Next)。
  • N: 跳到上一个搜索结果。
  • *: 向下搜索光标当前所在的单词。
  • #: 向上搜索光标当前所在的单词。

VirtualBoxVM_8AxnWVGnP9.png

Linux 帮助命令

有三种类型的帮助命令:

  • whatis 命令
  • 命令的 --help 选项
  • man 命令

whatis 命令

whatis 命令会给你一个关于命令的非常简短的单行描述。当你知道命令的名称但忘记了它的作用时,这个命令很有用。

示例:

1
whatis ls

这将输出类似以下内容: ls (1) - list directory contents (列出目录内容)

命令的 --help 选项

大多数命令都有一个 --help 选项,它提供了如何使用该命令的简短摘要,包括其语法和可用选项。这是一种在不离开终端的情况下快速记住如何使用命令的方法。

示例:

1
ls --help

这将打印出 ls 命令的用法和选项摘要。

WindowsTerminal_ryHM9TZ51l.png

man 命令

man 命令(manual 的缩写)显示命令的完整手册页。这是最详细的信息来源,提供了完整的描述、语法、选项、参数、示例和其他相关信息。

示例:

1
man ls

这将打开 ls 命令的手册页。

WindowsTerminal_dKcWRfgLTQ.png

区别

命令 内容 详细程度
whatis 一句话描述 最低
--help 用法和选项摘要 中等
man 完整的用户手册 最高

Linux 2 从命令行管理文件

关于文件系统基础、文件和目录操作、链接、以及I/O重定向和管道的核心概念。


1. 文件系统 (Filesystem) 基础

什么是文件系统?

  • 文件系统是操作系统用来管理文件和目录的一种机制。
  • 它控制着数据如何被存储和检索,实现对文件和目录的有序化、结构化管理。
  • 常见的类Unix文件系统类型包括:ext3, ext4, xfs

ls -l 输出详解

执行 ls -l 命令可以查看文件和目录的详细属性,输出结果通常包含9列:

VirtualBoxVM_2JATjTiWmo.png

  1. 文件类型和权限: 第一个字符表示类型(- 普通文件, d 目录, l 符号链接),后面9个字符表示权限。
  2. 硬链接数: 指向此文件inode的硬链接数量。
  3. 所有者 (Owner): 文件或目录的拥有者。
  4. 所属组 (Group): 文件或目录所属的用户组。
  5. 大小 (Size): 文件的大小(以字节为单位)。
  6. 最后修改月份: …
  7. 最后修改日期: …
  8. 最后修改时间: …
  9. 名称 (Name): 文件或目录的名称。

举个例子,如果我对-开头的文件使用cd命令,是会被拒绝的,因为这是一个普通文件而不是一个目录。


2. 核心文件/目录操作命令

创建

  • 创建文件:
    • touch <filename>: 创建一个空文件或更新现有文件的时间戳。
      VirtualBoxVM_fbQmsabdCL.png
    • cp <source> <destination>: 复制文件。
      VirtualBoxVM_IsxqxI6uee.png
    • vi <filename>: 使用 vi 编辑器创建并编辑文件。输入:进入命令行模式,wq退出并保存。vi/vim的更多详细操作我们暂且按下不表。
      VirtualBoxVM_lCQqIF4OsD.png
      VirtualBoxVM_rm1HpuZGCu.png
  • 创建目录:
    • mkdir <dirname>: 创建一个新目录。
      VirtualBoxVM_HjFMv0fGgi.png

维护

  • 复制: cp
    VirtualBoxVM_NPQoyFceDj.png
    你可以随时用man命令来查看关于这些命令的文档。
  • 删除: rm (文件), rmdir (空目录), rm -r (递归删除目录及其内容)
    VirtualBoxVM_hielyK9uBK.png
    VirtualBoxVM_yQLvdzhCH3.png
  • 移动/重命名: mv
    重命名也是使用mv命令,mv并不是单纯的移动。而是“修改指针”或者“复制并删除”。
    VirtualBoxVM_RJVHDT0lBs.png
  • 更改属组: chgrp
    VirtualBoxVM_D4Bj8wEr4H.png
  • 更改属主: chown
    VirtualBoxVM_gDnKuWtUBd.png

  • Inode: 可以理解为文件在硬盘上的唯一标识符,存储了文件的元数据(如权限、大小、位置等)。

硬链接 (ln)

  • 创建: ln <original_file> <hard_link_name>
  • 特性:
    • 实质上是给同一个 inode 创建了另一个文件名。
    • 删除、移动或重命名原始文件,不会影响硬链接,因为它直接指向 inode。
    • 不能跨文件系统创建。
    • 不能对目录创建硬链接。

软链接/符号链接 (ln -s)

  • 创建: ln -s <original_file> <soft_link_name>
  • 特性:
    • 相当于一个快捷方式,存储的是原始文件的路径。
    • 如果原始文件被删除、移动或重命名,软链接将失效(dangling link)。
    • 可以跨文件系统创建。
    • 可以对目录创建软链接。

Acrobat_r72maEYC26.png

VirtualBoxVM_270WwlUQZC.png

通过这张图我们可以看到,硬链接的inode号是一样的,而软链接的是不一样的。

VirtualBoxVM_TswrrY0LRT.png

如果我们rm掉~目录下的源文件,你发现那个硬链接的还是会在,硬链接数从2变为了1,而软链接的变成了悬空的指针(变成了红色高亮的断开链接)。


4. I/O 重定向与管道

Linux Shell中有三个标准流:

  • 文件描述符0: stdin (标准输入)
  • 文件描述符1: stdout (标准输出)
  • 文件描述符2: stderr (标准错误)

重定向 (Redirects)

  • 输出重定向:
    • >: 将命令的 标准输出 重定向到一个文件(覆盖文件原有内容)。> 符号默认就是操作 1 (stdout)
      • ls -l > listings.txt
        VirtualBoxVM_f7QGhtEPSz.png
    • >>: 将命令的 标准输出 追加到一个文件的末尾。
      • echo "Hello" >> listings.txt
        VirtualBoxVM_awiNI9DV83.png
  • 错误重定向:
    • 2>: 将命令的 标准错误 重定向到一个文件。
      • ls /nonexistent 2> error.log
        VirtualBoxVM_AXNVY779w6.png
  • 输入重定向:
    • <: 将文件的内容作为命令的 标准输入
      • cat < listings.txt
        VirtualBoxVM_2zVUYsvw02.png

管道 (Pipes)

  • 符号: |
  • 作用: 将一个命令的 stdout 直接连接到另一个命令的 stdin,实现命令的串联处理。
  • 示例:
    1
    2
    # 将 ls -l 的输出通过 more 命令进行分页显示
    ls -l | more
    VirtualBoxVM_wzmqbbh4BI.png

下载并安装虚拟机 + 配置环境

1 下载Orade Virtualbox

官网下载地址,选择自己的操作系统后进行安装。

本人使用的环境是windows环境。安装成功后运行如图。

FYnx3D6xKR.png

2 下载Linux发行版

这里我使用的是Red Hat Enterprise Linux。大概本教程会覆盖RHCSA的所有内容,所以这里使用redhat的发行版,当然你也可以使用其他主流的发行版。

对于个人开发者,可以使用免费的RHEL,需要在这里注册成为开发者。

之后来到这个页面点击 Download RHEL at no-cost就可以下载到iso了。

firefox_YoY3W83aF0.png

3 新建虚拟机

镜像和软件已经准备完毕,接下来我们开始安装。首先点击软件左上角的new,在弹出的对话框中写好自己的虚拟机名称VM Name,虚拟机目录VM Folder,使用的系统镜像ISO Image选择我们刚才下载的redhat的iso。

VirtualBox_PWb8k8f7Ib.png

请务必取消勾选静默安装Proceed with Unattended Installation

虚拟硬件推荐内存2GB,CPU两个。

VirtualBox_1b41FF3BsS.png

虚拟硬盘推荐至少20GB。

VirtualBox_KXqFlmAE04.png

之后点击右下角的fininsh

我们就可以看到这个新建号的虚拟机了。暂且为了方便虚拟机使用网络,我们选中这个虚拟机之后点击上方的Settings,在弹出的窗口中选择左侧的Network选项卡,在Adapter 1下选择Bridged Adapter

VirtualBox_3hrFwfdby3.png

4 安装RHEL

之后点击上方的绿色右箭头Start,即可运行虚拟机。

随后使用上下方向键,选择中间的Test this media & install Red Hat Enterprise Linux 10.0,按enter键确定。

VirtualBoxVM_9k8QSsOKGs.png

在经历了一阵检测和安装之后,你会看到下面的界面:

VirtualBoxVM_pMab7ODDDT.png

这里我推荐全部选择英文,避免任何因为中文字符可能出现的问题。随后点击右下角的continue

VirtualBoxVM_ZI81wmtEdt.png

可以看到有很多爆红的地方,我们一个一个来解决和配置。

首先第一个,我们需要连接到Red Hat。点击Connet to Red Hat之后输入自己的Redhat账户并验证即可。

VirtualBoxVM_8grO4cV4hh.png

登录后

VirtualBoxVM_h75CXEwlGL.png

然后我们在Installation Destination中选择我们的虚拟硬盘。

VirtualBoxVM_j8GL89TRqG.png

软件选取Software Selection中我们选择Server with GUI。右侧的附加选项我们不勾选。

VirtualBoxVM_TZzbWG2Wuh.png

Root账户是推荐的,这样至少可以保证一点安全性。

VirtualBoxVM_JDWJhKXgIo.png

下面的Create User我们可以创建一个常规用户。

VirtualBoxVM_OBQzS4hY5E.png

完成后我们就看不到红色的了,我们点击右下角开始安装!

VirtualBoxVM_wuBJVzZuu7.png

稍事等待…

VirtualBoxVM_yRLdzGI0hJ.png

然后我们就安装好了!点击右下角的重启就可以进入系统了。

VirtualBoxVM_gQHZdBXjQb.png

重启之后就可以输入密码进入到我们的系统了。

VirtualBoxVM_ssGe3fTPvw.png

1 两数之和

其实C++才是最适合研究算法的语言,但是我已经很多年没有碰cpp了,上一次碰的时候应该还是大二打第二次蓝桥杯的时候用的,或许我都快把万能头怎么写给忘掉了。

#include<bits/stdc++.h>
using namespace std;
结果居然是用Python写的这道题。因为最近几乎只是用过这一种语言。
一开始犯了很愚蠢的错误,我没有看清变量名,反而把初始化的数据结构名当作变量名了。
算法题的函数传参似乎都比较严格。在函数声明的时候就已经规范好了数据类型。
写了一个很基础的n^2遍历。

这里有一个时间复杂度更低的方法O(1)就是哈希表 hash table。
unordered_map
值得一提的是,我看了人家hashtable的c++题解之后,立刻提出了一个问题:
“在不考虑内部顺序和哈希冲突的情况下,hash table的功能是否和dictionary数据类型有点相似呢?”


是的,在不考虑内部顺序和哈希冲突的情况下,哈希表(hash table)的功能确实与字典(dictionary)数据类型非常相似。
实际上,Python的字典(dict)就是使用哈希表作为其底层实现的。这两者之间的关系非常紧密:

键值对存储:两者都是通过键值对(key-value pairs)来存储数据。
快速查找:两者都提供接近O(1)的时间复杂度进行查找操作。也就是说,无论字典有多大,查找一个键所需的时间基本是常数时间。
通过键访问:两者都是通过键来访问对应的值,而不是通过索引位置。

不过,当我们考虑内部顺序和哈希冲突时,确实会发现一些区别:

内部顺序:传统的哈希表通常不保证顺序,而Python的字典从Python 3.7开始保证了插入顺序。
哈希冲突处理:不同哈希表实现可能使用不同的哈希冲突解决策略,例如开放寻址法或链式哈希。Python的字典使用开放寻址法来处理哈希冲突。


class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hashtable;
        for (int i = 0; i < nums.size(); ++i) {
            auto it = hashtable.find(target - nums[i]);
            if (it != hashtable.end()) {
                return {it->second, i};
            }
            hashtable[nums[i]] = i;
        }
        return {};
    }
};

作者:御三五 🥇
链接:https://leetcode.cn/problems/two-sum/solutions/657225/tu-jie-guan-fang-tui-jian-ti-jie-liang-s-02xs/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        hashtable = dict()
        for i, num in enumerate(nums):
            if target - num in hashtable:
                return [hashtable[target - num], i]
            hashtable[nums[i]] = i
        return []

作者:御三五 🥇
链接:https://leetcode.cn/problems/two-sum/solutions/657225/tu-jie-guan-fang-tui-jian-ti-jie-liang-s-02xs/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        length = len(nums)
        for i in range(length):
            for j in range(i+1, length):
                if nums[i]+nums[j] == target:
                    result = [i,j]
                    return result

C++容器

对于.find() 方法对于所有 C++ 标准库中的 unordered_ 系列容器基本上都是一样的。这些容器包括:
-unordered_map
-unordered_multimap
-unordered_set
-unordered_multiset
所有这些容器的 .find() 都返回一个迭代器,如果找到了元素,返回指向该元素的迭代器,如果没找到元素,返回 容器.end()

时间复杂度:
平均情况:O(1)(常数时间)
最坏情况:O(n)(当哈希冲突严重时)

用法:

auto it = container.find(key);
if (it != container.end()) {
    // 找到了元素
}

这里auto是自动分配类型,因为这里是一个容器类型,通常就直接用auto自动适配了。
对于map而言,find()搜索的是key。

对于unordered_map<int,int> example访问key和value:
example->first 访问key
example->second 访问value
此外,如果要返回容器类型,那么是用{}来包裹值。

// 创建并初始化vector
vector<int> nums = {1, 2, 3, 4, 5};  // 初始化一个包含5个元素的vector

// 直接构造
vector<int> nums2 {1, 2, 3, 4, 5};   // 等同于上面的写法

// 函数返回
return {1, 2};                      // 如果函数返回类型是vector<int>,这会创建并返回包含两个元素的vector

C++11 引入了花括号初始化(uniform initialization)作为一种通用的初始化容器的方式
适用于所有标准容器类型(vector, map, set 等)

128 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

时间复杂度o(n)就意味着不能使用sorted遍历。因为排序的时间复杂度是o(nlogn)。

所以这就意味着一定要使用哈希表来完成。

思路很简单:

首先必须要转成set去重,比如如果样例中是[1,1,1,2,3,4],对于每个1都需要遍历一个O(n)的循环。

然后我们需要找到这个序列的起点和终点,我们从set里面抓一个出来,之后用哈希表找比这个数小1的数是否存在,直到找到这个数所在序列的最小数。

然后再重新递增检测,找到该序列中存在的最大的数。两者相减就可以得到这个序列长度。

序列长度一直取max就可以得到最长的连续序列长度了。

jTjorfrhcv.png

explorer_pgRUbPQJ5O.png

无重复字符的最大子串

暴力遍历

暴力的思路还是非常简单的,一个n^2的暴力循环。第一个循环是第一位是哪一位,第二个循环是后面满足条件的最大位数。
第一反应没能写出来是因为没有想到set。对于稍微有些特别的数据结构并不是特别敏感。

def lengthOfLongestSubstring(s: str) -> int:
    n = len(s)
    max_length = 0
    
    # 遍历每个可能的起始位置
    for i in range(n):
        # 使用集合来跟踪已经出现的字符
        char_set = set()
        current_length = 0
        
        # 从起始位置向后扩展
        for j in range(i, n):
            # 如果当前字符已经在集合中,表示遇到了重复字符
            if s[j] in char_set:
                break
            
            # 将当前字符添加到集合中,并增加当前子串长度
            char_set.add(s[j])
            current_length += 1
        
        # 更新最大长度
        max_length = max(max_length, current_length)
    
    return max_length

但是对于C++而言,这里又要用到stl标准库中的unordered_set<>容器。回想我当时学习C++的时候,几乎就没有怎么用过这些stl容器。。但是对于这种题目使用这种容器几乎是唯一解法,因为不使用stl会使其极度复杂,因为你需要自己定义这种结构体。
所以现在学习算法的时候我会顺带学习一下C++的常用stl。
https://www.runoob.com/cplusplus/cpp-libs-unordered_set.html

#include <string>
#include <unordered_set>
#include <algorithm>

class Solution {
public:
    int lengthOfLongestSubstring(std::string s) {
        int n = s.length();
        int maxLength = 0;
        
        // 遍历每个可能的起始位置
        for (int i = 0; i < n; i++) {
            // 使用unordered_set来跟踪已经出现的字符
            std::unordered_set<char> charSet;
            int currentLength = 0;
            
            // 从起始位置向后扩展
            for (int j = i; j < n; j++) {
                // 如果当前字符已经在集合中,表示遇到了重复字符
                if (charSet.find(s[j]) != charSet.end()) {
                    break;
                }
                
                // 将当前字符添加到集合中,并增加当前子串长度
                charSet.insert(s[j]);
                currentLength++;
            }
            
            // 更新最大长度
            maxLength = std::max(maxLength, currentLength);
        }
        
        return maxLength;
    }
};

滑动窗口

理解成一个小窗口在移动,一直划选着符合条件的内容。
窗口初始化为0,将第一个元素添加进窗口,随后检测第二个元素是否存在于窗口:
如果不存在则添加进窗口。如果存在则循环删除窗口内最左侧元素直到满足条件。
满足条件后记录目前的长度并和最大长度进行比较。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        if not s:return 0
        left = 0
        lookup = set()
        n = len(s)
        max_len = 0
        cur_len = 0
        for i in range(n):
            cur_len += 1
            while s[i] in lookup:
                lookup.remove(s[left])
                left += 1
                cur_len -= 1
            if cur_len > max_len:max_len = cur_len
            lookup.add(s[i])
        return max_len

作者:powcai
链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/solutions/3982/hua-dong-chuang-kou-by-powcai/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。  

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.size() == 0) return 0;
        unordered_set<char> lookup;
        int maxStr = 0;
        int left = 0;
        for(int i = 0; i < s.size(); i++){
            while (lookup.find(s[i]) != lookup.end()){
                lookup.erase(s[left]);
                left ++;
            }
            maxStr = max(maxStr,i-left+1);
            lookup.insert(s[i]);
    }
        return maxStr;
        
    }
};

作者:powcai
链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/solutions/3982/hua-dong-chuang-kou-by-powcai/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度分析:
乍一看,这段代码似乎有嵌套循环(外部for循环和内部while循环),可能让人觉得时间复杂度是O(n²)。但实际上,这个算法的总体时间复杂度是O(n),其中n是字符串的长度。
原因如下:

外部的for循环明确执行n次,每次处理字符串中的一个字符。
关键在于内部的while循环:虽然它看起来可能在最坏情况下对每个字符都执行多次,但实际上,字符串中的每个字符最多只会被处理两次:

一次是在外部for循环中被添加到窗口(lookup集合)
一次是在while循环中被移出窗口

左指针left在整个过程中最多移动n次(从0到n-1)。
每次执行while循环体,left指针都会向右移动一位。所以while循环体的总执行次数不会超过n次。

所以,虽然代码中有嵌套的循环结构,但内部while循环的总执行次数受到字符串长度n的限制,使得整体时间复杂度仍然是O(n)。
详细解释为什么while循环总执行次数不超过n:

每次执行while循环,left指针都会向右移动一位
left指针在整个算法执行过程中只会从0移动到最多n-1
因此,while循环的总执行次数不能超过n

空间复杂度:
空间复杂度是O(min(m, n)),其中m是字符集的大小,n是字符串长度。因为在最坏情况下,lookup集合最多包含min(m, n)个字符。
这就是为什么滑动窗口是解决这类问题的高效方法,它避免了暴力解法中的重复计算,将时间复杂度从O(n²)降低到O(n)。

此外,insert方法和erase方法分别是添加和删除。

无重复字符的最大子串

一眼就是使用哈希来做,python的话就是字典。
我第一想到的是用counter分别计算每个单词的counter然后找相同的。
这个思路是错误的,因为需要一个标记visited的数组并且需要反复多次遍历。

正解

注意到每个字母异位词实际上是sort之后相同的词汇。不妨把单词sorted之后得到的东西作为key,把原单词加入进value。

python

from collections import defaultdict
class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        dict = defaultdict(list)
        for word in strs:
            sorted_word = "".join(sorted(word))
            dict[sorted_word].append(word)
        return list(dict.values())

C++

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        unordered_map<string, vector<string>> m;
        for (string& s : strs) {
            string sorted_s = s;
            ranges::sort(sorted_s); // 把 s 排序,作为哈希表的 key
            m[sorted_s].push_back(s); // 排序后相同的字符串分到同一组
        }
        vector<vector<string>> ans;
        ans.reserve(m.size()); // 预分配空间
        for (auto& [_, value] : m) {
            ans.push_back(value); // 哈希表的 value 保存分组后的结果
        }
        return ans;
    }
};

复杂度分析

时间复杂度:O(nmlogm),其中 n 为 strs 的长度,m 为 strs[i] 的长度。每个字符串排序需要 O(mlogm) 的时间。我们有 n 个字符串,所以总的时间复杂度为 O(nmlogm)。
空间复杂度:O(nm)。

使用 Cloudflare R2 + ShareX 搭建个人/团队专属“永久”图床

本指南旨在提供一个从零开始,利用 Cloudflare R2 的免费套餐和 ShareX 的强大功能,搭建一个高性能、高可靠性、几乎零成本且数据完全由自己掌控的图床的完整流程。

最终效果:通过一个快捷键截图,图片自动上传到你的专属存储空间,或手动上传自己的图片,自动将 Markdown 格式的链接复制到剪贴板,实现无缝写作。


目录

  1. 第一部分:配置 Cloudflare R2 (云端存储)
  2. 第二部分:配置 ShareX (桌面客户端)
  3. 第三部分:优化 ShareX 工作流
  4. 常见问题 (F.A.Q) 与排错

第一部分:配置 Cloudflare R2 (云端存储)

首先来到Cloudflare开通R2存储。

1.1 创建 R2 存储桶 (Bucket)

存储桶是存放你所有图片的容器。

  1. 登录 Cloudflare 控制台,在左侧菜单进入 R2
  2. 点击 Create bucket (创建存储桶)
  3. Bucket name: 输入一个全局唯一的存储桶名称 (例如 your-org-images-2025)。
  4. Location: 保持默认的 Automatic
  5. 点击 Create bucket

1.2 开放存储桶的公开访问权限

为了让上传的图片能被外部访问,需要开启公共访问。

  1. 进入你刚创建的存储桶,点击顶部的 Settings (设置) 选项卡。
    1r0AU3aWkD.png
  2. 下方Public Development URL 部分,点击右侧的 Enable
  3. 输入确认信息。
    fgc4amk7S7.png
  4. 记下这里显示的 https://pub-....r2.dev 格式的 URL,这是你的公共访问域名。
    RmQwxSxpLi.png

1.3 创建用于上传的 API Token

API Token 是让 ShareX 有权限上传文件到 R2 的“钥匙”。

  1. 回到 R2 的主页(概览页面),点击右上角的 Manage API Tokens (管理 R2 API 令牌)
    CTzhiiSl04.png
  2. 点击 Create Account API token (创建 API 令牌)
    FBEzXXohz7.png
  3. Permissions (权限): 务必选择 Object Read & Write (对象读和写)。这是最关键的一步,只读权限会导致上传失败。
    xB4pYQOeEI.png
  4. 点击 Create API token
  5. ⚠️ 立即复制并保存! 页面上会显示 Access Key IDSecret Access Key这两个密钥只会出现这一次,请立刻将它们复制并粘贴到一个安全的地方。最下方的Default endpoints链接也需要保存一下。
    kg9E8tEozI.png

第二部分:配置 ShareX (桌面客户端)

2.1 下载并安装 ShareX

从官网下载最新版本:https://getsharex.com/

2.2 配置 S3 上传目标

这是整个配置过程的核心。

  1. 打开 ShareX,在主界面点击 Destination settings...
  2. 在弹出的窗口左侧选择 Amazon S3,在右侧输入你的配置信息。
  3. 按照下表精确填写你的 R2 信息:
ShareX 设置项 填写内容 说明
Access key ID 粘贴你保存的 Access Key ID
Secret access key 粘贴你保存的 Secret Access Key
Region 留空
Endpoints 留空
Endpoint https://<你的AccountID>.r2.cloudflarestorage.com 如果你在刚才保存过可以直接复制,切记最后没有/
Bucket name 你的 R2 存储桶名称 (例如 your-org-images-2025)
Upload path img/%y/%mo/%d/ img/年/月/日/ 格式存放图片,有助于管理。ShareX 的变量格式是 %。(也可自定义)
Use custom domain 勾选此项
(自定义域名输入框) https://<你记下的r2.dev公共URL> 注意:这里只填刚才你记下的Public Development URL,不要加后面的 $key$。ShareX 新版本会自动处理。

2.2.1 关键的高级 (Advanced) 设置

在 S3 配置窗口的最下方,找到 Advanced 区域,进行如下设置:

  • Set public-read ACL on file: 必须取消勾选。R2 不支持此功能,勾选会导致 403 Forbidden 错误。
  • Use path style request: 必须勾选。R2 需要这种格式的请求 URL。

2.3 将 S3 设置为默认图片上传器

  1. 回到 ShareX 主界面。
  2. 点击 Destinations -> Image uploader -> File uploader -> 选择 Amazon S3
    Code_nqStY1UhqR.png

第三部分:优化 ShareX 工作流

3.1 自动复制 Markdown 链接

  1. 在 ShareX 主界面,点击 Task settings...
  2. 在弹出的窗口左侧选择下方的 Advanced
    ShareX_ZWFVlZZu0W.png
  3. 接着,点击 After upload 下方的 ClipboardContentFormat
  4. 将里面的内容替换为markdown格式 ![$filename]($result)

3.2 打开自动复制到剪切板

  1. 在主页面的 After upload tasks -> 右侧选择 Copy URL to clipboard
    ShareX_zf6qftjnu6.png

3.3 修改热键

在主页面的 Hotkey settings 中可以修改热键,实现截图直接上传并复制markdown到剪贴板一条龙。


常见问题 (F.A.Q) 与排错

Q1: 上传时报错 (403) Forbidden,怎么办?
A1: 这是最常见的问题,请检查以下两项 S3 高级设置:

  1. 确保 Set public-read ACL on file 没有被勾选。
  2. 确保 Use path style request 已经被勾选。
  3. 如果依然报错,请重新生成一个权限为 Object Read & Write 的 API Token 并更新到 ShareX 中。

Q2: 我不是想上传截图,我想上传我自己本地的别的图片
A2:
使用左侧的upload选项卡: 在选项卡内选择你需要的上传内容。
ShareX_34TAgHco1T.png

Q3: PicGo 为什么不能用?
A3: 在我的测试中,PicGo 的 S3 插件与 Cloudflare R2 存在一些兼容性问题,导致文件名和路径处理不正确。ShareX 的 S3 实现更标准,是目前更可靠的选择。

0%