数据结构的汇编实现
在应用程序编程中,数据结构的应用十分广泛。它们常被用于算法目的,实现诸如栈、队列和堆等结构,也可用于基于键的数据存储,也就是所谓的“字典”。本文将探讨如何在汇编中实现链表、哈希表、双向链表和二叉树。
1. 链表
链表是由一系列节点组成的结构。以下是一个链表的示例:
I ·I 4 1
I 21 191 +
这个链表包含4个节点,每个节点有一个数据值和一个指向下一个节点的指针。链表的最后一个节点的指针为NULL(值为0),用实心圆表示。链表本身由一个指针表示。我们可以将链表的第一个指针放在一个框中并命名,这样能更完整地展示链表。
这个链表中的数据值没有明显的顺序,可能是无序的,也可能按插入时间排序。由于在链表头部插入新节点非常容易,所以链表可能按插入时间降序排列。
链表通过存储在标记为
list
的内存位置的指针来引用。维护和使用链表的代码中,链表上的节点没有特定的标签,只能通过链表中的指针来访问这些节点。
1.1 链表节点结构
链表节点包含两个字段:一个数据值和一个指向下一个节点的指针。使用YASM的结构定义如下:
struc node
n_value resq 1
n_next resq 1
align 8
ends true
虽然在这个结构中有两个四字时不需要对齐指令,但它可以避免后续可能的混淆。
1.2 创建空链表
设计容器结构时,首先要决定如何表示空容器。在这个链表设计中,我们选择使用NULL指针来表示空链表。尽管这种方法很简单,但编写一个创建空链表的函数可能会更方便。
newlist:
xor eax, eax
ret
1.3 向链表中插入数字
将空链表实现为NULL指针在插入操作时会带来一个小问题。每次插入都在链表头部进行,这意味着每次插入后链表的起始指针都会存储一个新的指针。有两种方法可以处理这个问题:一种是将指针的地址传递给插入函数;另一种是让插入函数返回新的指针,由插入代码在返回时分配新指针。为了避免指针地址的问题,下面是插入代码:
insert:
.list equ 0
.k equ 8
push rbp
mov rbp, rsp
sub rsp, 16
mov [rsp + .list], rdi
mov [rsp + .k], rsi
mov edi, node_size
call malloc
mov r8, [rsp + .list]
mov [rax + n_next], r8
mov r9, [rsp + .k]
mov [rax + n_value], r9
leave
ret
1.4 遍历链表
遍历链表需要使用类似
mov rbx, [rbx + n_next]
的指令,从一个节点的指针移动到下一个节点的指针。我们首先检查指针是否为NULL,如果不是则进入循环。处理完一个节点后,移动指针,如果指针不为NULL则继续循环。下面的打印函数遍历链表并打印每个数据项:
print:
segment .data
.print_fmt: db "%ld ", 0
.newline: db 0x0a, 0
segment .text
.rbx equ 0
.more:
.done:
push rbp
mov rbp, rsp
sub rsp, 16
mov [rsp + .rbx], rbx
cmp rdi, 0
je .done
mov rbx, rdi
lea rdi, [.print_fmt]
mov rsi, [rbx + n_value]
xor eax, eax
call printf
mov rbx, [rbx + n_next]
cmp rbx, 0
jne .more
lea rdi, [.newline]
xor eax, eax
call printf
mov rbx, [rsp + .rbx]
leave
ret
最后,我们有一个主函数,它创建一个链表,使用
scanf
读取值,将这些值插入链表,并在每次插入后打印链表:
main:
.list equ 0
.k equ 8
segment .data
.scanf_fmt: db "%ld", 0
segment .text
push rbp
mov rbp, rsp
sub rsp, 16
call newlist
mov [rsp + .list], rax
.more:
lea rdi, [.scanf_fmt]
lea rsi, [rsp + .k]
xor eax, eax
call scanf
cmp rax, 1
jne .done
mov rdi, [rsp + .list]
mov rsi, [rsp + .k]
call insert
mov [rsp + .list], rax
mov rdi, rax
call print
jmp .more
.done:
leave
ret
以下是使用该程序输入1到5的示例会话:
1
1
2
2 1
3
3 2 1
4
4 3 2 1
5
5 4 3 2 1
可以看到,最后打印的数字位于链表的开头。通过添加一个获取并移除(弹出)链表第一个元素的函数,我们可以将这个链表变成一个栈。
2. 双向链表
双向链表的每个节点有两个指针:一个指向下一个节点,另一个指向前一个节点。如果将双向链表设计为循环链表,并在链表开头保留一个未使用的节点,那么管理双向链表会变得非常简单。以下是一个包含4个数据节点的双向链表示例:
list
X
4
12
16
19
c
-
变量
list
指向链表的第一个节点,即“头节点”。头节点有一个值,但我们不会使用这个值。每个节点的上指针指向下一个节点,下指针指向前一个节点。头节点的前指针指向链表的最后一个节点。这种设计使得双向链表可以实现栈(后进先出)、队列(先进先出)或双端队列。其主要优点是链表永远不会真正为空,虽然在逻辑上可能为空,但头节点始终存在。而且,一旦链表创建完成,头节点的指针就不会改变。
2.1 双向链表节点结构
双向链表节点包含三个字段:一个数据值、一个指向下一个节点的指针和一个指向前一个节点的指针。使用YASM的结构定义如下:
struc node
n_value resq 1
n_next resq 1
n_prev resq 1
align 8
ends true
2.2 创建新链表
创建新双向链表的代码会分配一个新节点,并将其下一个和前一个指针都指向自身。调用函数会收到一个在程序执行期间不会改变的指针。以下是创建代码:
newlist:
push rbp
mov rbp, rsp
mov edi, node_size
call malloc
mov [rax + n_next], rax
mov [rax + n_prev], rax
leave
ret
返回时,空链表的样子如下:
list
2.3 在链表头部插入节点
要在链表头部插入一个新节点,需要将头节点的下一个指针放入新节点的下一个槽中,并将头节点下一个节点的前一个指针放入新节点的前一个槽中。完成这些操作后,让头节点指向新节点,头节点原来的下一个节点指向新节点。插入代码如下:
insert:
.list equ 0
.k equ 8
push rbp
mov rbp, rsp
sub rsp, 16
mov [rsp + .list], rdi
mov [rsp + .k], rsi
mov edi, node_size
call malloc
mov r8, [rsp + .list]
mov r9, [r8 + n_next]
mov [rax + n_next], r9
mov [rax + n_prev], r8
mov [r8 + n_next], rax
mov [r9 + n_prev], rax
mov r9, [rsp + .k]
mov [rax + n_value], r9
leave
ret
2.4 遍历双向链表
双向链表的遍历与单链表有些相似。我们需要跳过头节点,并将当前指针与头节点的指针进行比较,以检测链表的结束。以下是打印链表的代码:
print:
segment .data
.print_fmt: db "%ld ", 0
.newline: db 0x0a, 0
segment .text
.list equ 0
.rbx equ 8
push rbp
mov rbp, rsp
sub rsp, 16
mov [rsp + .rbx], rbx
mov [rsp + .list], rdi
mov rbx, [rdi + n_next]
cmp rbx, [rsp + .list]
je .done
.more:
lea rdi, [.print_fmt]
mov rsi, [rbx + n_value]
call printf
mov rbx, [rbx + n_next]
cmp rbx, [rsp + .list]
jne .more
.done:
lea rdi, [.newline]
call printf
mov rbx, [rsp + .rbx]
leave
ret
3. 哈希表
哈希表是实现字典的一种高效方式。其基本思想是为字典中的每个键计算一个哈希值,目的是将键均匀分布在一个数组中。理想的哈希函数能将每个键映射到哈希数组中的唯一位置,但这很难实现,因此我们需要处理键的“冲突”。
处理冲突的最简单方法是为哈希数组的每个位置使用一个链表。例如:
0
1
2
3
4
5
6
7
8
在这个哈希表中,键12、4、16和9的哈希值都为1,它们被放在哈希数组位置1的链表中;键13和8的哈希值都为3,它们被放在位置3的链表中;其余键分别映射到5和7。
哈希的一个关键问题是开发一个好的哈希函数。哈希函数应该看起来几乎是随机的,对于同一个键,每次调用时必须计算出相同的值。但哈希值本身并不重要,重要的是键在链表上的分布,我们希望有很多短链表。这意味着数组的大小至少应该和预期的键的数量一样大,这样在使用好的哈希函数时,链表通常会很短。
3.1 整数的哈希函数
一般建议哈希表的大小为质数。但如果用作键的数字没有潜在的模式,这并不是很重要,此时可以简单地使用
n mod t
,其中
n
是键,
t
是数组大小。如果存在很多相同数字的倍数这样的模式,使用质数作为
t
是有意义的。以下是示例代码中的哈希函数:
hash:
mov rax, rdi
and rax, 0xff
ret
在示例中,表的大小为256,使用
and
操作实现了
n mod 256
。
3.2 字符串的哈希函数
对于字符串,一个好的哈希函数是将字符串视为多项式的系数,并为某个质数
n
计算
p(n)
的值。在下面的代码中,我们使用质数191进行计算。计算出多项式的值后,可以使用表的大小进行取模运算(示例代码中表的大小为100000)。
int hash(unsigned char *S)
{
unsigned long h = 0;
int i = 0;
while (S[i]) {
h = h * 191 + S[i];
i++;
}
return h % 100000;
}
3.3 哈希表节点结构和数组
示例哈希表的大小为256,因此程序开始时需要一个包含256个NULL指针的数组。由于这个数组比较小,可以在数据段中实现。对于更实际的程序,需要一个哈希表创建函数来分配数组并将其初始化为0。以下是数组的声明和每个数组位置的链表的结构定义:
segment .data
table times 256 dq 0
struc node
n_value resq 1
n_next resq 1
align 8
ends true
3.4 在哈希表中查找值
哈希表的基本目的是存储与键相关的数据。在示例哈希表中,我们只存储键。下面的查找函数会在哈希表中搜索一个键,如果找到则返回包含该键的节点的指针,否则返回0。更实际的程序可能会返回与键相关的数据的指针。
find:
.n equ 0
push rbp
mov rbp, rsp
sub rsp, 16
mov [rsp + .n], rdi
call hash
mov rax, [table + rax * 8]
mov rdi, [rsp + .n]
cmp rax, 0
je .done
.more:
cmp rdi, [rax + n_value]
je .done
mov rax, [rax + n_next]
cmp rax, 0
jne .more
.done:
leave
ret
3.5 插入键到哈希表
将键插入哈希表的代码首先调用查找函数,以避免重复插入同一个键。如果找到该键,则跳过插入代码;如果未找到,则调用哈希函数确定要添加键的链表的索引,分配一个新节点的内存,并将其插入链表的头部。
insert:
.n equ 0
.h equ 8
push rbp
mov rbp, rsp
sub rsp, 16
mov [rsp + .n], rdi
call find
cmp rax, 0
jne .found
mov rdi, [rsp + .n]
call hash
mov [rsp + .h], rax
mov rdi, node_size
call malloc
mov r9, [rsp + .h]
mov r8, [table + r9 * 8]
mov [rax + n_next], r8
mov r8, [rsp + .n]
mov [rax + n_value], r8
mov [table + r9 * 8], rax
.found:
leave
ret
3.6 打印哈希表
打印函数会遍历从0到255的索引,打印索引编号和每个非空链表上的键。它使用寄存器
r12
和
r13
来安全地存储循环计数器和链表指针,这样比使用在每次
printf
调用时都需要保存和恢复的寄存器更方便。在函数开始和结束时需要压入和弹出这两个寄存器,以保持栈的正确对齐。
print:
push rbp
mov rbp, rsp
push r12
push r13
xor r12, r12
.more_table:
mov r13, [table + r12 * 8]
cmp r13, 0
je .empty
segment .data
.print1 db "list %3d : ", 0
segment .text
lea rdi, [.print1]
mov rsi, r12
call printf
.more_list:
segment .data
.print2 db "%ld ", 0
segment .text
lea rdi, [.print2]
mov rsi, [r13 + n_value]
call printf
mov r13, [r13 + n_next]
cmp r13, 0
jne .more_list
segment .data
.print3 db 0x0a, 0
segment .text
lea rdi, [.print3]
call printf
.empty:
inc r12
cmp r12, 256
jl .more_table
pop r13
pop r12
leave
ret
3.7 测试哈希表
哈希表的主函数使用
scanf
读取数字,将它们插入哈希表,并在每次插入后打印哈希表的内容:
main:
.k equ 0
segment .data
.scanf_fmt: db "%ld", 0
segment .text
push rbp
mov rbp, rsp
sub rsp, 16
.more:
lea rdi, [.scanf_fmt]
lea rsi, [rsp + .k]
call scanf
cmp rax, 1
jne .done
mov rdi, [rsp + .k]
call insert
call print
jmp .more
.done:
leave
ret
插入1、2、3、4、5、256、257、258、260、513、1025和1028后,哈希表的打印内容如下:
list 0 : 256
list 1 : 1025 513 257 1
list 2 : 258 2
list 3 : 3
list 4 : 1028 260 4
list 5 : 5
4. 二叉树
二叉树是一种可能包含多个节点的结构,有一个根节点,根节点可以有左子节点、右子节点或两者都有。树中的每个节点都可以有左子节点和右子节点。
通常,二叉树会对节点中的键进行排序。例如,在一个二叉树中,每个节点将键分为小于该节点键的键(在左子树中)和大于该节点键的键(在右子树中)。这种有序的二叉树,通常称为二叉搜索树,使得在快速搜索键的同时,还能按升序或降序遍历节点。
这里我们将介绍一个整数键的二叉树,键的排序规则是较小的键在左边,较大的键在右边。首先是用于树的结构。
综上所述,本文详细介绍了链表、双向链表、哈希表和二叉树等数据结构在汇编中的实现方法,包括节点结构的定义、创建、插入、遍历等操作,这些数据结构在算法和数据存储方面都有重要的应用。
数据结构的汇编实现
4.1 二叉树节点结构
二叉树节点包含三个字段:一个数据值、一个指向左子节点的指针和一个指向右子节点的指针。使用YASM的结构定义如下:
struc node
n_value resq 1
n_left resq 1
n_right resq 1
align 8
ends true
4.2 创建新节点
创建新二叉树节点的代码会分配一个新节点的内存。以下是创建代码:
new_node:
push rbp
mov rbp, rsp
mov edi, node_size
call malloc
mov [rax + n_left], 0
mov [rax + n_right], 0
leave
ret
4.3 插入节点到二叉树
将节点插入二叉树时,需要根据节点键的大小,将其插入到合适的位置。如果键小于当前节点的键,则插入到左子树;如果键大于当前节点的键,则插入到右子树。以下是插入代码:
insert:
.root equ 0
.k equ 8
push rbp
mov rbp, rsp
sub rsp, 16
mov [rsp + .root], rdi
mov [rsp + .k], rsi
mov rdi, node_size
call malloc
mov r8, [rsp + .k]
mov [rax + n_value], r8
mov [rax + n_left], 0
mov [rax + n_right], 0
mov rbx, [rsp + .root]
cmp rbx, 0
je .return_new
.loop:
cmp r8, [rbx + n_value]
jl .left
jmp .right
.left:
cmp [rbx + n_left], 0
je .insert_left
mov rbx, [rbx + n_left]
jmp .loop
.right:
cmp [rbx + n_right], 0
je .insert_right
mov rbx, [rbx + n_right]
jmp .loop
.insert_left:
mov [rbx + n_left], rax
jmp .done
.insert_right:
mov [rbx + n_right], rax
jmp .done
.return_new:
mov [rsp + .root], rax
.done:
mov rax, [rsp + .root]
leave
ret
4.4 遍历二叉树
二叉树有多种遍历方式,这里介绍中序遍历(左 - 根 - 右)。中序遍历可以按升序输出节点的键。以下是中序遍历的代码:
inorder:
.root equ 0
push rbp
mov rbp, rsp
sub rsp, 16
mov [rsp + .root], rdi
mov rbx, [rsp + .root]
cmp rbx, 0
je .done
push rbx
mov rdi, [rbx + n_left]
call inorder
pop rbx
segment .data
.print_fmt: db "%ld ", 0
segment .text
lea rdi, [.print_fmt]
mov rsi, [rbx + n_value]
xor eax, eax
call printf
mov rdi, [rbx + n_right]
call inorder
.done:
leave
ret
4.5 测试二叉树
以下是一个测试二叉树插入和遍历的主函数:
main:
.root equ 0
.k equ 8
segment .data
.scanf_fmt: db "%ld", 0
segment .text
push rbp
mov rbp, rsp
sub rsp, 16
xor rax, rax
mov [rsp + .root], rax
.more:
lea rdi, [.scanf_fmt]
lea rsi, [rsp + .k]
xor eax, eax
call scanf
cmp rax, 1
jne .done
mov rdi, [rsp + .root]
mov rsi, [rsp + .k]
call insert
mov [rsp + .root], rax
mov rdi, [rsp + .root]
call inorder
segment .data
.newline: db 0x0a, 0
segment .text
lea rdi, [.newline]
xor eax, eax
call printf
jmp .more
.done:
leave
ret
数据结构操作流程总结
为了更清晰地展示各种数据结构的操作流程,下面通过表格和流程图进行总结。
数据结构操作表格
| 数据结构 | 创建操作 | 插入操作 | 遍历操作 |
|---|---|---|---|
| 链表 |
使用
newlist
函数,将指针置为NULL
|
使用
insert
函数,在头部插入
|
使用
print
函数,通过指针移动遍历
|
| 双向链表 |
使用
newlist
函数,分配节点并将前后指针指向自身
|
使用
insert
函数,在头部插入并调整指针
|
使用
print
函数,跳过头节点并比较指针遍历
|
| 哈希表 | 初始化数组为NULL指针 |
使用
insert
函数,先查找避免重复,再插入
|
使用
print
函数,遍历数组和链表
|
| 二叉树 |
使用
new_node
函数,分配节点并初始化子节点指针
|
使用
insert
函数,根据键大小插入到合适位置
|
使用
inorder
函数,中序遍历
|
链表插入操作流程图
graph TD;
A[开始] --> B[分配新节点内存];
B --> C[获取链表指针和插入值];
C --> D[将新节点的下一个指针指向原链表头];
D --> E[更新链表头指针为新节点];
E --> F[结束];
二叉树插入操作流程图
graph TD;
A[开始] --> B[分配新节点内存并初始化值];
B --> C[获取根节点指针和插入值];
C --> D{根节点是否为空};
D -- 是 --> E[返回新节点作为根节点];
D -- 否 --> F{插入值 < 当前节点值};
F -- 是 --> G{左子节点是否为空};
G -- 是 --> H[插入到左子节点];
G -- 否 --> I[递归插入到左子树];
F -- 否 --> J{右子节点是否为空};
J -- 是 --> K[插入到右子节点];
J -- 否 --> L[递归插入到右子树];
H --> M[结束];
I --> M;
K --> M;
L --> M;
E --> M;
总结
本文详细介绍了链表、双向链表、哈希表和二叉树等数据结构在汇编中的实现。通过具体的代码示例,展示了这些数据结构的节点结构定义、创建、插入和遍历等操作。这些数据结构在算法和数据存储方面有着广泛的应用,不同的数据结构适用于不同的场景。
- 链表适用于需要频繁插入和删除操作的场景,尤其是在头部插入。
- 双向链表在支持双向遍历和实现栈、队列等数据结构方面具有优势。
- 哈希表能够高效地实现字典,通过合理的哈希函数和冲突处理,提高查找和插入的效率。
- 二叉树,特别是二叉搜索树,在需要有序存储和快速查找时非常有用。
掌握这些数据结构的实现和操作,对于提高程序的性能和效率至关重要。在实际应用中,需要根据具体的需求选择合适的数据结构。
超级会员免费看
1272

被折叠的 条评论
为什么被折叠?



