Linux_初识Linux

Linux学习内容

  1. 基本操作
  2. gcc/g++(程序如何编译)
  3. 工程文件管理makefile
  4. 调试gdb
  5. 库文件
  6. 进程:复制进程fork、替换进程
  7. 进程间通信(比如QQ互发信息):管道、共享内存、信号量
  8. 线程:同步、并发
  9. 网络编程:跨主机发送数据(最好有计算机网络基础),TCP、HTTP、I/O复用方法

初识内容

  1. 目录结构:Linux中的文件夹称为目录
  2. 文件类型
  3. 权限
  4. 命令

总结

  1. 与Windows相比:Linux多用命令行,我们要掌握基本的命令。
  2. 了解系统的目录结构,是个倒状树,记住常见的四五个目录的作用。
  3. 用户存放文件,要放到自己的家目录。

Linux目录结构

目录 描述 全拼
/ 根目录
bin 二进制可执行程序,常用的命令就存放到这了。 binary, 表示二进制可执行文件
dev 硬件设备文件,把硬件设备抽象成为了文件 device, 设备
lib 这个目录里存放着系统最基本的动态库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都需要用到这些共享库。 library, 库
mnt 临时挂载移动设备 mount
proc 将内存中进程的信息映射到了文件中 process, 进程
home 普通用户的家目录,以用户名为准,如/home/xcg
root 管理员的家目录
boot 存放系统启动相关的内核文件
etc 系统的配置文件 et cetera, “等等”
usr 系统用户工具和程序。在系统运行过程中,不经常改变的内容,比如说安装的软件。存放所有命令、库、手册等共享资源。子目录有bin(用户命令)、sbin(超级用户使用的高级管理程序和系统守护程序)、includelib(用户后来安装的库文件)、src(内核源代码) 据说是user的缩写,也据说是Unix Shared Resources的缩写
var usr目录下的软件产生的数据,会存放于此;除此之外还有邮件、系统日志等。因此经常改变
proc 每一个进程启动后,都会在里面创建一个名为进程ID号的目录,保存进程相关的信息。其中包含fd,即进程打开的文件、socket。

etc

etc不是什么缩写,是"and so on"的意思,来源于法语的"et cetera",翻译成中文就是"等等"的意思。至于为什么在/etc下面存放配置文件,按照原始的UNIX的说法这下面放的都是一堆零零碎碎的东西,就叫etc,这其实是个历史遗留。

这个目录一般用来存放程序所需的整个文件系统的配置文件。

/etc目录包含很多文件,许多网络配置文件在其中:

  1. /etc/rcor/etc/rc.dor/etc/rc*.d:启动、或改变运行级时运行的scripts或scripts的目录。
  2. /etc/passwd:用户数据库,其中的域给出了用户名、真实姓名、家目录、加密的口令和用户的其他信息。
  3. /etc/fstab:启动时mount -a命令(在/etc/rc 或等效的启动文件中)自动mount的文件系统列表。Linux下也包括用swapon -a启用的swap区的信息。
  4. /etc/group:类似/etc/passwd,但说明的不是用户而是组
  5. /etc/issue:getty在登录提示符前的输出信息,通常包括系统的一段短说明或欢迎信息,内容由系统管理员确定。
  6. /etc/magic:file的配置文件,包含不同文件格式的说明,file基于它猜测文件类型。
  7. /etc/mtab:当前安装的文件系统列表,由scripts初始化,并由mount命令自动更新。需要一个当前安装的文件系统的列表时使用,例如df命令。
  8. /etc/shadow:在安装了影子口令软件的系统上的影子口令文件,影子口令文件将/etc/passwd文件中的加密口令移动到/etc/shadow中,而后者只对root可读,这使破译口令更困难。
  9. /etc/login.defs:login命令的配置文件。
  10. /etc/profile, /etc/csh.login, /etc/csh.cshrc:登录或启动时Bourne或Cshells执行的文件,这允许系统管理员为所有用户建立全局缺省环境。
  11. /etc/shells:列出可信任的shell.chsh命令允许用户在本文件指定范围内改变登录shell。提供一台机器FTP服务的服务进程ftpd检查用户shell是否列在 /etc/shells文件中,如果不是将不允许该用户登录

usr

之前一直没有怎么关注过它,反正程序都是安装在里边的,也没有什么值得追根溯源的东西。那么usr到底是什么的缩写呢,它又是怎么来的呢?讨论中,大部分观点认为:

  1. usr是user的缩写;
  2. usr是unix system resources的缩写;
  3. usr是unix software resources的缩写。
  4. usr是unix shared resources

根据常识判断,是user缩写的可能性不大,因为和/home冲突了。不过是system resources还是software resources的缩写还真不好说。特此查了好多东西,却发现竟然连wikipedia也模棱两可。/usr是linux系统核心所在,包含了所有的共享文件。
它是unix系统中最重要的目录之一,涵盖了二进位制文件,各种文件,各种标头文件,还有各种库文件,还有诸多程序,例如ftp,telnet等等。

曾经的/usr还是使用者的家目录,存放着各种使用者文件,但现在已经被/home取代了(例如/usr/someone已经改为/home/someone)。
现代的/usr只专门存放各种程序和资料,使用者目录已经转移。虽然/usr名称未改,不过其含义已经从“使用者目录” 变成了“unix系统资源” 目录。值得注意的是,在一些unix系统上,仍然把/usr/someone当做使用者家目录,如Minix。

/usr文件系统经常很大,因为所有程序安装在这里。/usr里的所有文件一般来自Linux distribution;本地安装的程序和其他东西在/usr/local下。这样可能在升级新版系统或新distribution时无须重新安装全部程序。

由于/usr中的文件不和特定的计算机相关,也不会在通常使用中修改,因此可以通过互联网共享这个目录(文件系统),这样,当管理员安装了新的站群软件之后,所有共享这一文件系统的计算机均可以使用新的站群软件。

至此,真相大白。看来就像前一阵子的/var/run移到/run一样。

真的是不看不知道,一看吓一跳呀。原来linux几经进化,好多目录的诞生和用途已经产生了根本的变化。

  • /usr/bin: 所有可执行文件,如gcc等(指不包含在 /sbin/bin 内的);
  • /usr/include: 各种标头文件,编译文件等时需要使用;
    • /usr/include/’package-name’ : 程序特定的标头文件;
  • /usr/lib: 所有可执行文件所需要的库文件;
  • /usr/local: 这里主要存放那些手动安装的站群软件,即不是通过“新立得” 或 apt-get 安装的站群软件。 它和/usr目录具有相类似的目录结构 。让站群软件包管理器来管理/usr目录,而把自定义的指令码(scripts)放到/usr/local目录下面,应该是个不错的主意。
  • /usr/games: 曾经包含游戏等文件,现在很少用到;
  • /usr/man: man 手册,已经移至 /usr/share/man
  • /usr/sbin: 类似 /sbin,root 可以执行。但此目录不包含在环境变数 $PATH 中,它包含的程序类似于 chroot, useradd, in.tftpd and pppconfig
  • /usr/share: 它包含了各种程序间的共享文件,如字型,图示,文件等。(/usr/local 对应的目录是 /usr/loca/share);
    • /usr/share/doc : 类似应用程序的 man 手册。它包含程序的说明文件,预设配置文件等;
    • /usr/share/info : 不常用,已经被 man 代替;
    • /usr/share/man : app 的 manual;
    • /usr/share/icons : 应用程序的图示等文件,分为png,svg等多种格式;
  • /usr/src : linux 核心的原始码和说明文件等;
    • /usr/src/linux : linux 原始码;

文件属性

image-20220512112348998

文件类型

文件类型 符号表示
普通文件(文档) -
目录文件(文件夹) d
管道文件 p
设备文件 cb
链接文件 l
套接字文件 s

文件权限

文件权限 符号表示 二进制表示 十进制数
无权限 - 0000 0
可读 r 0100 4
可写 w 0010 2
可执行 x 0001 1

chmod改权限

所有权范围 符号表示
自己(属主) u
同组人 g
其他人 o
1
2
3
#chmod 改权限
chmod u-w main #属主 去除 写权限
chmod o+w main #其他人 增加 写权限

基础命令

基本文件操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 文件操作基本命令
cd #切换目录
pwd #显示当前位置
ls #显示当前目录下有哪些文件
ls -l #显示详细信息
ls -a #显示隐藏文件
touch #创建普通文件
touch a.c
mkdir tmp #创建tmp目录(文件夹)
cp
rm
mv
cat
more/less/head/tail # 优先学习more、tail
vi/vim #编辑文件

ls

1
2
3
ls 	#显示当前目录下有哪些文件
ls -l #显示详细信息
ls -a #显示隐藏文件

cd

1
2
3
4
5
6
7
8
#cd 	#切换目录

cd \ #切换位置到根目录下 #也可写作cd /
cd tmp #进入tmp这个目录(文件夹)
cd ~ #回到家目录
cd .. #返回上一层
#绝对路径
#相对路径

cp/mv

1
2
3
4
5
6
7
8
9
10
11
12
cp	#拷贝
cp a.c b.c #拷贝普通文件
cp -r tmp tmp1 #拷贝文件夹,包括文件夹的内容,参数要加-r
cp file.txt tmp
cp file.txt tmp/file.txt

mv #1.移动 2.重命名
#mv对文件夹的操作不需要加-r
mv a.c b.c #将a.c重命名为b.c
mv dir1 dir2#将目录1重命名为目录2
mv b.c dir#如果已经有dir这个目录,那么我们将会把b.c移动到dir里;如果不存在dir文件,则会重命名b.c为dir
mv b.c dir/a.c #如果已经有dir这个目录,那么我们将会把b.c移动到dir里并重命名为a.c

rm

1
2
3
4
5
6
rm	#删除
rm -f #不给提示报错的删除
rm -d #删除文件夹(只能删除空的)
rm -r #递归删除文件夹(包含里面所有内容)
rm -rf #-r和-f的组合用法,强有力,一定要谨慎使用
rmdir #删除空文件夹

高级文件操作

cat/more/less/head/tail

1
2
3
4
5
6
7
8
9
10
cat #1.打印文件内容 2.向文件输入内容 3.合并文件
cat a.c #打印文件内容
cat > a.txt#键盘输入内容,重定向到文件里。Ctrl+D结束输入。如果a.txt不存在,自动创建。
cat a.c b.c > d.txt#把a.c和b.c的内容按顺序写到d.txt中。合并到另一文件中。
more #分屏幕显示,适于输出超过一个屏幕的文件内容,但只能看一次,空格是翻页,回车是下一行
less #在more的基础上,可以回滚。退出按q
head #默认打印文件前10行
head -3 passwd #打印前三行
tail #默认打印文件后10行
tail -3 passwd #打印后三行

tar

1
tar zxf name.tar.gz #解包

find

1
find /home/xcg -name a.c #找某个目录下有无a.c

grep

vi/vim

关于vi的配置

vi默认的tab缩进是制表符,怎么改为空格?

对于使用vim的程序员来说,shiftwidth,tabstop,softtabstop绝对是经常接触的三个缩进因素。能否有方便美观的,整体化的缩进,主要是由他们相互间的配合决定。在经过一段时间试用后,总结一下我的设置经验。

shiftwidth

这个是用于程序中自动缩进所使用的空白长度指示的。一般来说为了保持程序的美观,和下面的参数最好一致。同时它也是符号移位长度的制定者。

tabstop

定义tab所等同的空格长度,一般来说最好设置成8,因为如果是其它值的话,可能引起文件在打印之类的场合中看起来很别扭。除非你设置了 expandtab模式,也就是把tabs转换成空格,这样的话就不会一起混淆,不过毕竟制表符为8是最常用最普遍的设置,所以一般还是不要改。

softtabstop

如果我们希望改变程序中的缩进怎么办?shiftwidth和tabstop不一样的话,你会发现程序比较难看的。这时候,softtabstop就起作用了。可以从vim的说明中看到,一旦设置了softtabstop的值时,你按下tab键,插入的是空格和tab制表符的混合,具体如何混合取决于你设定的softtabstop,举个例子,如果设定softtabstop=8,那么按下tab键,插入的就是正常的一个制表符;如果设定 softtabstop=16,那么插入的就是两个制表符;如果softtabstop=12,那么插入的就是一个制表符加上4个空格;如果 softtabstop=4呢?那么一开始,插入的就是4个空格,此时一旦你再按下一次tab,这次的四个空格就会和上次的四个空格组合起来变成一个制表符。换句话说,softtabstop是“逢8空格进1制表符”,前提是你tabstop=8。

关于expandtab

举个例子,在多人一起开发项目时,为了使代码风格尽量保持一致,一般不允许在代码使用TAB符,而以4个空格代之。我们可以编辑一个文件,包含下面的内容:
set shiftwidth=4
set expandtab

然后把下面的命令加入到.vimrc中:
autocmd FileType c,cpp set shiftwidth=4 | set expandtab

就可以只在编辑c和cpp文件时实行这种设置了

删除掉每一行末尾的空格:

行末:$
行首:^
空格:\s
行末空格:\s\+$
行首空格:^\+\s
有些人认为行末的空格是无用,浪费而难看的。要删除这些每行后面多余的空格,可以执行如下命令:
:%s/\s\+$//
命令前面指明范围是"%“,所以这会作用于整个文件。“substitute” 命令的匹配模式是
"\s\+$"。这表示行末($)前的一个或者多个(+)空格(\s)。后面我们会介绍怎样
写这样的模式。
替换命令的 “to” 部分是空的:”//"。这样就会删除那些匹配的空白字符。
另一种没有用的空格是 Tab 前面的字符。通常这可以删除而不影响格式。但并不是总这样!所以,你最好手工删除它。执行如下命令:
/
你什么都看不见,其实这是一个空格加一个 TAB 键。相当于 “/”。现在,
你可以用 “x” 删除多余的空格,并保证格式没有改变。接着你可以用 “n” 找到下一个
位置并重复这个操作。

插入模式(编辑模式)

命令模式--i/a/o/s-->插入模式--ESC-->命令模式

  • i:在光标所在字符前开始插入
  • a:在光标所在字符后开始插入
  • o:在光标所在行的下面另起一新行插入
  • s:删除光标所在的字符并开始插入
  • I:在光标所在行的行首开始插入 如果行首有空格则在空格之后插入
  • A:在光标所在行的行尾开始插入
  • O:在光标所在行的上面另起一行开始插入
  • S:删除光标所在行并开始插入

命令模式

底行命令模式/插入模式--ESC-->命令模式

1
2
3
4
5
6
7
8
r #只替换一个
R #一直替换 按esc结束
x #删除一个字符,但是不能删除空行
cc #清除整行并进入插入模式
shift+c或C #清楚本行光标及以后的内容,并进入插入模式
/ #对文本进行全文向下搜索字符串string 如:/text
? #对文本进行全文向上搜索字符串string 如:?text
G #切换到末尾行

vi打开文件,以搜索"port"为例:

从开头处开始搜索:/port

从结尾处开始搜索:?port

向下搜索:n

向上搜索:N,或者shift+n,或者shift+#

底行命令模式

命令模式--:-->末行模式-->

1:wq保存并退出 2:**q!**强制不保存退出 3:w只保存 4:q仅退出

1
:n #切换到第n行

练习

  1. 复制51到60行,并且粘贴到最后一行后面

    1
    2
    3
    4
    50G		#先跳转到50行
    10yy #复制光标下10行
    G #跳转到文件最末
    p #粘贴复制内容
  2. 删除11到30行之间的20行

    1
    2
    10G		#先跳转到10行
    20dd #删除光标下20行
  3. 移动到第29行,并且删除15个字符。

    1
    2
    29G		#先跳转到29行
    15x #x表示删除光标所在位置的一个字符,X表示删除光标所在位置的前面一个字符,前面加数字可以删除指定个。

gcc

关于编译链接更多内容请查看文章“Linux_库”。

1
gcc -o main main.c

进程操作

1
2
3
4
5
# 进程操作基本命令
ps #查看当前终端中运行的进程
kill #结束进程
jobs #查看后台运行的进程
& #放到后台运行

帮助手册命令-man

1
2
3
man #帮助文档 1.命令 2.系统调用 3.库函数
#printf 既有命令也有库函数,而man默认输出命令相关文档,所以要输出库函数时,我们要加 3
man 3 printf

快捷键

功能 快捷键
打开终端 Ctrl+Alt+T

一些需要搜索才知道的操作

  1. 网上下载的deb安装包双击后提示”软件无法安装:不支持“。

    1
    sudo dpkg -i google-chrome-stable_current_amd64.deb
  2. 关闭没有必要的动画特效

    1
    gsettings set org.gnome.desktop.interface enable-animations false
  3. 找不到环境文件夹——链接

    https://stackoverflow.com/questions/3655306/ubuntu-usr-bin-env-python-no-such-file-or-directory

    1
    2
    whereis python3
    sudo ln -s /usr/bin/python3 /usr/bin/python

Redis_dict

引例

图书馆借书问题,图书馆有成千上万本图书,最初,需要有一个本子记录哪一天学生记录的情况。最初以日期为编号记录某一天来借书的情况。当有人来还书时,需要先问清借书者是几号来借的,然后还要在这天的记录里依次找到具体信息。如果借书者清楚自己是几号借的还好说,但是如果不记得了呢?比如说他只记得好像是上上个月来借的,那么就需要一天一天的从头到尾查找记录,显然效率是低下的。
image-20210807233939705
那么我们可以如此改进:我们搞一个000 0000 0000 ~ 199 9999 9999页的本子,某同学借书时让他提供电话号码,以电话号码为基准记录信息。待还书时,只需要报电话号则可以直接找到记录。但此方法不现实,因为本子太厚,要花费的空间太大。
image-20210807234740133
再改进:做一个1000页的本子,因为电话号码的范围远远大于页数,我们要想办法使电话号码映射到页数,则这个映射方案则称为“哈希函数”。如A同学电话号码为137 3456 9090,可取后三位090,存到90页。B同学电话号码为186 7890 1234,取后三位234,存到234页。
image-20210807235252067
此时又来了一个C同学,电话号码为156 9090 1234,虽然关键码与B同学不一样,但位置信息一样,则称之为“哈希冲突”。

哈希表

什么是哈希表

严蔚敏版本的数据结构教材中指出,在诸如线性表、树结构中,“记录”中结构中的相对位置是随机的,和“记录”的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较。比如,数组存放的从小到大的10个数据,要用二分查找法找出某value的下标值就需要一直探测。这一类查找方法建立在“比较”的基础上。在顺序查找时,比较的结果为“=”与“≠”两种可能;在折半查找、二叉排序树查找和B树查找时,比较的结果为“<”、“=”和“>”3种可能。查找的效率依赖于查找过程中所进行的比较次数。

我们所向往的理想的情况是希望不经过任何比较,一次存取便能得到所查记录,那就必须在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个惟一的存储位置相对应。因而在查找时只要根据这个对应关系f找到给定值K的“像”f(K)便能找到其存储位置。若结构中存在关键字和K相等的记录,则必定在f(K)这个存储位置上,由此不需要比较便可直接取得所查记录。在此我们称这个对应关系f为哈希(Hash)函数,由此思想建立的表称为哈希表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<assert.h>
#define NIL -1
#define m 13
typedef int KeyType;
struct ElemType
{
KeyType key;
void* ptr; //value
};
typedef struct
{
ElemType data[m];
int cursize;
}HashTable;

image-20210808011308669

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void Init_HashTable(HashTable* pt)
{
assert(pt != nullptr);
pt->cursize = 0;
for(int i=0;i<m;++i)
{
pt->data[i].key = NIL;
pt->data[i].ptr = nullptr;
}
}
int Hash(KeyType kx)
{
return kx%m;
}
bool Insert_Item(HashTable* pt, ElemType item)
{
assert(pt != nullptr);
int index = Hash(item.key);
pt->data[index] = item;
pt->cursize += 1;
}
int main()
{
HashTable ht;
Init_HashTable(&ht);
int ar[]={12,23,25,8,19,29};
int n=sizeof(ar)/sizeof(ar[0]);
for(int i=0;i<n;++i)
{
struct ElemType item = {ar[i],nullptr};
bool tag = Insert_Item(&ht,item);
}
}

但是这种Insert_Item函数是有问题的,没有解决哈希冲突问题,比如在插入key为25的元素时会替换掉前期已经插入的key为12的元素。

image-20210808013500159

以下来通过 (增量)线性探测法来解决哈希冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int Inc(int i)
{
return i;
}
int Hash_Inc(KeyType kx, int i)
{
return (Hash(kx) + Inc(i))%m;
}
bool Insert_Item(HashTable* pt, ElemType item)
{
assert(pt != nullptr);
for(int i = 0;i<m;++i)
{
int index = Hash_Inc(item.key, i);
if(pt->data[i].key == NIL)//随着Inc更迭,找到一个空位置
{
pt->data[index] = item;
pt->cursize += 1;
return true;
}
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
HashTable ht;
Init_HashTable(&ht);
int ar[]={12,15,28,23,25,38,36,49,14};
int n=sizeof(ar)/sizeof(ar[0]);
for(int i=0;i<n;++i)
{
struct ElemType item = {ar[i],nullptr};
bool tag = Insert_Item(&ht,item);
}
}

image-20210808015044675

这种线性探测法虽然解决了哈希冲突,但最终的数据分布太堆积化。可以通过平方探测法改善此问题。

1
2
3
4
int Inc(int i)
{
return i*i;
}

但无论是线性探测法存储还是平方探测法存储,在这种纯顺序表(数组)中实现哈希表会遇到一个严重的问题!即插入数据后,不能轻易删除某一个数据,因为删除了某一数据后,查找另外的数据就会出现数据断层导致不能正常探测。比如下图:如果把3下标的28删除,则查找key为49的元素时,第一次探测49%13=10,23不等于49,第二次探测(线性探测,下标+1),36不等于49,第三次探测,12不等于49,……,直到探测到下标3,发现key值为-1。如果我们停止探测返回,则是错误的;所以我们只能遇到-1不停止继续探测,那么就失去了哈希表的意义,这相当于在查某个key对应的value时有可能把整个表都遍历了一遍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void* FindValue(HashTable* pt, KeyType kx)
{
assert(pt != nullptr);
for(int i = 0;i<m;++i)
{
int pos = Hash_Inc(kx,i);
if(pt->data[pos].key==kx)
{
return pt->data[pos].ptr;
}
if(pt->data[pos].key==NIL)
{
return nullptr;
}
}
}

image-20210808020035825

链地址法

所以我们要改变这种存储哈希表的方式,改用链地址法存储。(视频01:27:42)

image-20210808191518194

看图设计结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<assert.h>
#define m 13
typedef int KeyType;
typedef struct ElemType
{
KeyType key;
void* ptr;
}ElemType;
typedef struct HashNode
{
ElemType data;
struct HashNode* next;
}HashNode;
typedef struct HashTable
{
HashNode* table[m];
int cursize;
}HashTable;

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int Hash(KeyType kx)
{
return kx % m;
}
void Init_Hash(HashTable* pt)
{
assert(pt!=nullptr);
for(int i = 0;i<m;++i)
{
pt->table[i] = nullptr;
}
pt->cursize = 0;
}
void Insert_Item(HashTable* pt, ElemType item)//但是此函数没有解决存在相同key时的情况
{
assert(pt!=nullptr);
int index = Hash(item.key);
HashNode* s = (HashNode*)malloc(sizeof(HashNode));
if(nullptr == s) exit(1);
s->data = item;
s->next = pt->table[index];
pt->table[index] = s;
pt->cursize += 1;
}
int main()
{
int ar[]={1,55,19,20,10,11,14,68,84,23,27,79};
int n = sizeof(ar)/sizeof(ar[0]);
HashTable ht;
Init_Hash(&ht);
for(int i = 0;i<n;++i)
{
ElemType elem = {ar[i],nullptr};
Insert_Item(&ht,elem);
}
}

API

Init

1
2
3
4
5
6
7
8
9
void Init_Hash(HashTable* pt)
{
assert(pt != nullptr);
for(int i = 0;i<m;++i)
{
pt->table[i]=nullptr;
}
pt->cursize=0;
}

Insert

1
2
3
4
5
6
7
8
9
10
11
12
13
void Insert_Item(HashTable* pt, ElemType item)
{
assert(pt != nullptr);
int index = Hash(item.key);
HashNode* pnode = (HashNode*)malloc(sizeof(*pnode));
if (nullptr == pnode) exit(1);
pnode->data = item;//讲外部elem封装到新建结点的数据域
//以下两步为入表操作,采用头插法,时间复杂度为O(1),不用判头结点是否为空
pnode->next = pt->table[index];
pt->table[index] = pnode;

pt->cursize++;//现有大小+1
}

FindNode

查看字典中是否已存在某key值对应的节点并返回。

1
2
3
4
5
6
7
8
9
10
HashNode* FindNode(HashTable* pt, KeyType kx)
{
int index = Hash(kx);
HashNode* p = pt->data[index];
while (p != nullptr && p->item.key != kx)
{
p = p->next;
}
return p;
}

Clear

自己写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//free掉所有节点,HashTable的指针数组数据全赋空
void ClearTable(HashTable* pt)
{
for (int i = 0;i < m;++i)
{
HashNode* p = pt->data[i];
HashNode* q = nullptr;
while (p)
{
q = p->next;
free(p);
pt->cursize--;
p = q;
}
pt->data[i] = nullptr;
}
}

老师写的

1
2
3
4
5
6
7
8
9
10
11
12
13
void ClearHash(HashTable* pt)
{
for(int i = 0;i<m;++i)
{
while(pt->data[i]!=nullptr)
{
HashNode* q = pt->data[i];
pt->data[i]=q->next;
free(q);
}
}
pt->cursize=0;
}

Remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//删除某一节点,需用到两个指针变量,一前一后。
void RemoveNode(HashTable* pt, KeyType kx)
{

int index = Hash(kx);
HashNode* p = pt->data[index];
if (p == nullptr) exit(1);//不存在要删除的key的对应节点,因为Table数组此下标的值既然为空则肯定不会有key值相应的节点存在
HashNode* q = p->next;
//若删除的节点为头结点,则特殊处理
if (p->item.key == kx)
{
pt->data[index] = q;
free(p);
}
else//
{
while (q != nullptr && q->item.key != kx)
{
p = p->next;
q = q->next;
}
if (q == nullptr) exit(1);//不存在要删除的key的对应节点,因为遍历这一条链后不存在相同的key值对应的节点。
else//q->item.key == kx
{
p->next = q->next;
free(q);
}
}
pt->cursize--;
return;
}

老师写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//Remove节点更老套的写法:
bool Remove(HashTable* pt,KeyType kx)
{
assert(pt!=nullptr);
int index=Hash(kx);
HashNode* pr=nullptr;//前驱指针
HashNode* p=pt->table[index];
while(p!=nullptr)
{
if(p->item.key==kx)
{
if(pr!=nullptr)
{
pr->next=p->next;
}
else
{
pt->table[index]=p->next;
}
free(p);
pt->total-=1;
return true;
}
pr=p;
p=p->next;
}
return false;
}

习题

定义大小为100 的整型数组,使用随机函数给数组元素赋值。数值的范围是1 … 100 ,并且不容许重复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int* My_NonRepeating_RandArr(int* ar, int n)
{
assert(ar != nullptr);
int br[101];
for (int i = 0;i < n + 1;++i)
{
br[i]=0;
}
int temp;
srand(time(NULL));
int i = 0;
do
{
temp = rand() % 100 + 1;
if (br[temp] != 1)
{
br[temp] = 1;
ar[i] = temp;
++i;
}
} while (i<n);
return ar;
}

如果题中数组大小改为1000000,那么数组开辟空间就很大,如果仍然按照旧的方式去做肯定是对程序不利的。考虑用哈希算法解决!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include<stdio.h>
#include<assert.h>
#define m 100
typedef int KeyType;
typedef struct ElemType
{
KeyType key;
void* ptr;
}ElemType;
typedef struct HashNode
{
ElemType data;
struct HashNode* next;
}HashNode;
typedef struct HashTable
{
HashNode* table[m];
int cursize;
}HashTable;
void Init_Hash(HashTable* pt)
{
assert(pt != nullptr);
for(int i = 0;i<m;++i)
{
pt->table[i]=nullptr;
}
pt->cursize=0;
}
void Insert_Item(HashTable* pt, ElemType item)
{
assert(pt != nullptr);
int index = Hash(item.key);
HashNode* pnode = (HashNode*)malloc(sizeof(*pnode));
if (nullptr == pnode) exit(1);
pnode->data = item;//讲外部elem封装到新建结点的数据域
//以下两步为入表操作,采用头插法,时间复杂度为O(1),不用判头结点是否为空
pnode->next = pt->table[index];
pt->table[index] = pnode;

pt->cursize++;//现有大小+1
}
int Hash(KeyType kx)
{
return kx%m;
}
HashNode* FindValue(HashTable* pt, KeyType kx)
{
int index = Hash(kx);
HashNode* p = pt->table[index];
while(p != nullptr && p->data.key != kx)
{
p=p->next;
}
return p;
}
int* My_NonRepeating_Hash(int* ar, int n)
{
assert(ar != nullptr);
HashTable ht;
Init_Hash(&ht);
int temp;
srand(time(NULL));
int i = 0;
do
{
temp = rand()* rand() % 1000000 + 1;
if (FindValue(&ht, temp) == nullptr || *(int*)(FindValue(&ht, temp)->data.ptr) != 1)
{
ElemType item = { temp,new int(1) };
Insert_Item(&ht, item);
ar[i] = temp;
++i;
}
} while (i < n);
return ar;
}
int main()
{

}

老师写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include<assert.h>
#define m 13
#define INC 2
typedef int KeyType;
typedef struct
{
KeyType key;
void* ptr;
}ElemType;
typedef struct HashNode
{
ElemType data;
struct HashNode* next;
}HashNode;
typedef struct HashTable
{
HashNode** table;
int capacity;
int total;
}HashTable;
int Hash(HashTable* pt, KeyType kx)
{
//return kx % m; //原始不考虑增容的情况下默认%m
return kx % pt->capacity;
}
void Init_Hash(HashTable* pt)
{
assert(pt!=nullptr);
pt->table = (HashNode**)malloc(sizeof(HashNode*)*m);
if(pt->table == nullptr)exit(EXIT_FAILURE);
pt->capacity = m;
for(int i = 0;i<pt->capacity;++i)
{
pt->table[i] = nullptr;
}
pt->total = 0;
}
HashNode* FindNode(HashTable* pt, KeyType kx)
{
int index = Hash(pt,kx);
HashNode* p = pt->table[index];
while(p != nullptr && p->data.key != kx)
{
p=p->next;
}
return p;
}
bool Inc(HashTable* pt)//对哈希表的增容操作
{
int incsize = pt->capacity * INC;//INC为增容系数,默认为2,incsize表示增容后的新大小,为此前哈希表容量的2倍。
HashNode** newtable = (HashNode**)malloc(sizeof(HashNode*)*incsize);
if(newtable == nullptr)return false;
for(int i = 0;i<incsize;++i)
{
newtable[i] = nullptr;
}
pt->captacity = incsize;
//以上,开辟新的哈希表完毕,下面要把旧表复制到新表
int oldsize = pt->capacity;
for(int i = 0;i<oldsize;++i)
{
if(pt->table[i]!=nullptr)
{

}
}
}
bool Insert_Item(HashTable* pt, ElemType item)
{
assert(pt!=nullptr);
HashNode* p = FindNode(pt,item.key);
if(p!=nullptr)return false;//哈希表中已存在相同key值的节点
if(pt->total > pt->capacity && !Inc(pt))//需要增容但没有增容成功
{
return false;
}
int index
}

C++就一个东西引用返回,B站的老师永远搞不清楚

Redis字典

字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。
在字典中,一个键(key)可以和一个值(value)进行关联(或者说将键映射为值),这些关联的键和值就称为键值对。
字典中的每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对,等等。
字典经常作为一种数据结构内置在很多高级编程语言里面,但Redis所使用的C语言并没有内置这种数据结构,因此 Redis构建了自己的字典实现。
字典在Redis中的应用相当广泛,比如 Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。

与相似结构对比

HashTable是无序的;
Map是有序的;HashMap是无序的,底层有红黑树实现。因此Map效率较低。

字典的实现

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

哈希表

Redis字典所使用的哈希表由dict.h/dictht结构定义:

1
2
3
4
5
6
7
typedef struct dictht
{
dictEntry** table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemark;//哈希表大小掩码,用于计算索引值。总是等于size-1
unsigned long used;//表中已有节点的数量
}dictht;

成员table的功能是一个数组(指向首元素的指针),数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。

image-20210818030916272

哈希表节点

即dictEntry结构。

1
2
3
4
5
6
7
8
9
10
11
typedef struct dictEntry
{
void* key;
union
{
void* val;
uint64_tu64;
int64_ts64;
}v;
struct dictEntry* next;
}dictEntry;

image-20210818031224095

字典

Redis中的字典由dict.h/dict结构表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct dictType 
{
// 计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;
typedef struct dict
{
dictType* type;//结构体指针,用于存放特定类型的操作函数
void* privdata;//私有数据
dictht ht[2];//内部定义了两个哈希表
int rehashidx;//当没有在rehash时值为-1
}dict;

image-20210818032212233

面试

  1. 渐进式增容/缩容
  2. 一致性哈希–解决负载均衡的问题。比如登陆项目。一千万个用户同时登陆。
  3. 操作系统一定要开始看了,内存管理、进程管理、线程管理。

dictDelete

  1. 删除第一节点
  2. 删除后个数会少,要处理

time

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
* 返回以毫秒为单位的 UNIX 时间戳
*
* T = O(1)
*/
long long timeInMilliseconds(void)
{
struct timeval tv;

gettimeofday(&tv,NULL);
return (((long long)tv.tv_sec)*1000)+(tv.tv_usec/1000);
}
/* Rehash for an amount of time between ms milliseconds and ms+1 milliseconds */
/*
* 在给定毫秒数内,以 100 步为单位,对字典进行 rehash 。
*
* T = O(N)
*/
int dictRehashMilliseconds(dict *d, int ms)
{
// 记录开始时间
long long start = timeInMilliseconds();
int rehashes = 0;

while(dictRehash(d,100)) {
rehashes += 100;
// 如果时间已过,跳出
if (timeInMilliseconds()-start > ms) break;
}

return rehashes;
}