数据结构散列表和数据结构的学习路线规划

散列函数

散列函数的作用是将关键字映射到表格中的一个存储地址。

一个好的散列函数应满足:

1. 简单:计算 not 太复杂,避免消耗过多资源;

2. 随机性:可以均匀映射关键字到所有存储地址,避免"聚集"现象;

3. 一致性:对同一关键字,始终映射到同一个地址;

4. 全方位:散列函数的范围应能覆盖表格的整个存储空间,以充分利用空间。

具体散列函数方法主要有:

1. 直接定址法:h(key) = key mod N,N为表格大小。这是最简单方法但要求N足够大,否则冲突较多。

2. 分而治之法:将关键字分成几部分分别计算散列值,最后组合。如5位数分3位数和2位数计算散列值。这方法可以减少直接定址法中的冲突,利用关键字的高位和低位特征。

3. 平方取中法:取关键字的平方值的中间几位作为散列值。如5位数取平方值中间3位数。这种方法可以较好地分散关键字,减少冲突。

4. 折叠法:将关键字分成几个部分,这几部分的值相加得到散列值。如5位数分3个2位数,3个2位数的值之和为散列值。这是一种常用和较好的散列方法。

5. 除留余数法:散列函数为h(key) = key mod p, p是一个不大于表格大小的质数。这种方法可以较均匀地映射关键字,并且计算简单。质数p的选择会影响散列效果。

除此之外,还有随机数法、拉链法等。选取何种方法根据关键字范围和组成来获得最佳散列效果:最均匀映射和最小冲突。当冲突较多时,我们可以使用拉链法或再散列法来解决冲突,提高散列表性能。

这里给出Python代码实操散列表和散列函数:

首先,我们定义一个散列表HashTable类:

class HashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [None] * self.size

然后定义一个简单的散列函数,这里采用除留余数法:

def hash_func(self, key):
        return key % self.size

实现插入操作,采用拉链法解决冲突:

def insert(self, key, value):
        hash_key = self.hash_func(key)
        if self.table[hash_key] is None:
            self.table[hash_key] = [(key, value)]
        else:
            self.table[hash_key].append((key, value))

实现搜索操作:

 def search(self, key):
        hash_key = self.hash_func(key)
        if self.table[hash_key] is not None:
            for pair in self.table[hash_key]:
                if pair[0] == key:
                    return pair[1]
        return None

实现删除操作:

 def delete(self, key):
        hash_key = self.hash_func(key)
        if self.table[hash_key] is not None:
            for i in range(len(self.table[hash_key])):
                if self.table[hash_key][i][0] == key:
                    self.table[hash_key].pop(i)
                    break

我们可以这样使用HashTable:

ht = HashTable()
ht.insert(1, 'a')
ht.insert(2, 'b')
ht.insert(3, 'c')

print(ht.search(1))
ht.delete(2)
print(ht.search(2))

输出:
a
None

这是一个简单的散列表实现,采用拉链法解决冲突,并实现了基本的插入、搜索和删除操作。希望通过这个代码示例,可以加深您对散列表原理与实现的理解。

冲突解决

在散列表中,冲突指两个或多个关键字映射到同一个存储地址的情况。由于散列函数的随机性,想完全避免冲突是不现实的,所以我们需要采取一定的方法来解决冲突,提高散列表的性能。

常见的冲突解决方法有:

1. 开放定址法:也叫探测再散列法。当有冲突时,通过某种探测序列在表格中查找下一个空闲单元,将记录存入。常见的探测序列有:

- 线性探测:h(key, i) = (h'(key) + i) mod N,i是探测次数;
- 平方探测:h(key, i) = (h'(key) + i*i) mod N;
- 双散列探测:h(key, i) = (h1'(key) + i*h2'(key)) mod N,h1和h2是两个散列函数。

开放定址法的缺点是,当表格较满时,查找时间会增大;并且在删除时会出现"坏空洞"的问题。

2. 拉链法:每个存储地址包含一个链表,当有冲突时,将记录存储在链表中。这种方法解决了开放定址法中的问题,但是增加了链表操作的时间消耗。这是一种常用和较好的冲突解决方法。

3. 再散列法:当有冲突时,使用第二个散列函数计算另一个存储地址,直到找到空闲位置。这种方法的时间复杂度较高,一般不常用。除此之外,还有其他方法如使用公共溢出区等。 Choosing 一种冲突解决方法需要综合考虑表格大小,关键字的特征以及实际应用中的性能要求。  一般来说,当表格较小或关键字范围较大时,拉链法是比较常用的方法;当关键字较少或性能要求较高时,开放定址法也可以得到很好应用。

这里给出采用开放定址法解决散列表冲突的Python代码实现:

首先,我们仍然定义一个散列表HashTable类:

class HashTable:
    def __init__(self, size=10):
        self.size = size
        self.table = [None] * self.size

然后定义散列函数,这里也采用除留余数法:

def hash_func(self, key):
        return key % self.size

实现插入操作,这里采用开放定址法中的线性探测解决冲突:

def insert(self, key, value):
        hash_key = self.hash_func(key)
        if self.table[hash_key] is None:
            self.table[hash_key] = (key, value)
        else:
            i = 1
            while self.table[(hash_key + i) % self.size] is not None:
                i += 1
            self.table[(hash_key + i) % self.size] = (key, value)

实现搜索操作:

def search(self, key):
        hash_key = self.hash_func(key)
        i = 1
        while self.table[(hash_key + i) % self.size] is not None:
            if self.table[(hash_key + i) % self.size][0] == key:
                return self.table[(hash_key + i) % self.size][1]
            i += 1
        return None 

实现删除操作:

def delete(self, key):
        hash_key = self.hash_func(key)
        i = 1
        while self.table[(hash_key + i) % self.size] is not None:
            if self.table[(hash_key + i) % self.size][0] == key:
                self.table[(hash_key + i) % self.size] = None
            i += 1

我们可以这样使用:

ht = HashTable()
ht.insert(1, 'a')
ht.insert(2, 'b')  
ht.insert(3, 'c')

print(ht.search(1))
ht.delete(2)  
print(ht.search(2))

输出:
a
None 这是采用开放定址法中的线性探测解决散列表冲突的实现,相比拉链法增加了一定的性能开销,但是节省了额外空间。通过代码实现可以加深对散列表冲突解决方法的理解。

动态扩容

动态扩容的核心原理是:当散列表的装填因子(已插入元素个数/表格大小)达到一定阈值后,自动扩大表格大小,并将原表格中的所有元素重新散列插入新表格。这可以避免散列表的装填因子过高,保证查询性能,同时节省空间且重新散列可以得到更均匀的分布。常见的动态扩容方法是将原表格大小加倍。扩容时,需要重新计算每个元素在新表格中的散列地址,并插入元素。扩容操作会带来额外的计算开销,所以需要选择一个适当的装填因子阈值,一般在0.7-0.8之间。

下面是动态扩容操作实现的详细流程:

1. 定义一个装填因子阈值,如0.75;

2. 在每次插入元素时,判断当前表格的装填因子是否达到阈值;如果达到,进行扩容操作;

3. 扩容时,新表格大小为原表格大小的2倍。如原大小为10,则新大小为20;

4. 遍历原表格中的所有元素,重新计算其在新表格中的散列地址,并插入新表格;

5. 将新表格替换原表格;

6. 更新表格大小为新大小。插入元素时,需要使用新大小计算散列地址。

7. 由于表格扩大,搜索和删除元素时也需要使用新表格和新大小,重新计算元素对应的散列地址。

下面是HashTable类中动态扩容的实现:

class HashTable:
    def __init__(self, size=10):
        self.size = size
        self.threshold = 0.75
        self.table = [None] * self.size
        
    # 插入元素
    def insert(self, key, value):
        # 判断是否扩容
        if len(self.table) / self.size >= self.threshold:
            # 扩容,大小加倍
            self.size *= 2
            new_table = [None] * self.size
            
            # 重新散列插入元素
            for pair in self.table:
                hash_key = self.hash_func(pair[0])
                new_table[hash_key] = pair
                
            # 使用新表格和大小
            self.table = new_table
            
        # 散列插入    
        hash_key = self.hash_func(key)
        self.table[hash_key] = (key, value)
        
    # 搜索和删除元素也需要判断是否扩容并重新计算散列值

位图

位图(Bitmap)是一种节省空间的散列表表示方法。它适用于关键字范围较小的应用场景。位图的原理很简单:它将一个较大的散列表空间压缩为一个字节或多个字节。每个字节的每一位表示散列表的一个存储单元。0表示该单元为空,1表示已插入元素。比如关键字范围在0-255之间,我们可以用一个字节的8位来表示这个散列表,每一位对应一个存储单元。如果要插入关键字5,则将第5位设置为1。要查找关键字30,则检查第30位的值。利用位图,可以节省较大空间,代价是关键字范围受限制在一个字节或多个字节的位数之内。此外,位图不支持存储关联值,只能作为存在性查询来使用。

下面给出位图在Python中的简单实现:

class BitMap:
    def __init__(self, size):
        self.size = size
        # 计算需要的字节数
        self.bytes = self.size // 8
        if self.size % 8 != 0:
            self.bytes += 1 
        self.bitmap = [0] * self.bytes
        
    # 插入元素        
    def insert(self, key):
        # 计算关键字对应的字节和位
        byte_index = key // 8
        bit_index = key % 8
        # 将对应位设置为1
        self.bitmap[byte_index] |= (1 << bit_index)
        
    # 查找元素        
    def search(self, key):
        # 计算关键字对应的字节和位
        byte_index = key // 8
        bit_index = key % 8
        # 检查对应位的值
        return self.bitmap[byte_index] & (1 << bit_index) > 0 

我们可以这样使用:

bm = BitMap(256)
bm.insert(5)
bm.insert(30)
print(bm.search(5))   # True
print(bm.search(10))  # False

位图是一种节省空间的散列表表示方法,适用于关键字范围较小的场景。

本期的学习内容以上已经完全交代清楚。不过学习数据结构应该要有整体的学习路线把握节奏,一下为学习的路线:

学习数据结构应遵循以下原则:

1. 内容广度和深度并重。既要了解常见数据结构、算法的基本思想和实现,也要深入理解时间复杂度分析、运用场景等核心概念。

2. 理论和实践并重。看书理解概念仅是入门,实现、编码、 practice 才是最关键的一步。要通过大量实践来巩固理论,培养运用能力。

3. 持之以恒。数据结构与算法的学习是一个漫长的过程,需要不断实践和回顾。一遍学习不会真正掌握,需要反复去理解,编码,运用。

更详细的学习计划和时间划分可以如下:

1. 入门基础:数组、链表、栈和队列(1-2周)。理解思想,实现基本操作。

2. 散列表和树形结构(2-4周)。散列表包括散列函数、冲突解决、动态扩容、位图等。树形结构包括二叉树、平衡树、B树等。实现并分析时间复杂度。

3. 排序与查找(2-3周)。分别实现不同的排序算法和查找算法,分析时间复杂度,理解适用场景。

4. 项目实践(2-4周)。选择感兴趣的项目,利用所学数据结构与算法来实现,加深理解和掌握。如LRU缓存、文件索引、搜索引擎等。

5. 扩展学习(3-6周)。框架与库的使用,如STL;算法导论中的高级算法;其他高级数据结构如Trie树、并查集、线段树等。

6. 持续复习(长期)。通过自己总结的知识点,实现代码等,不断回顾和巩固。

综上,要系统掌握比较全面的数据结构与算法,至少需要3-6个月的时间。

详细的学习知识点掌握:

1. 入门基础(2周)- 理解数组、链表、栈和队列的思想和实现
- 实现数组、链表、栈和队列,并分析时间复杂度
- 编写测试用例测试数据结构代码

2. 散列表(Hash Table)(4周)  - 理解散列函数的种类和原理,如除留余数法、折叠法等
- 理解散列表的冲突解决方法,如开放定址法、拉链法等 
- 实现散列表,并测试不同的散列函数和冲突解决方法 
- 深入理解动态扩容、装填因子及其实现
- 实现位图,理解与散列表的区别和使用场景

3. 树形结构(4周)  - 理解二叉树、完全二叉树的概念和实现 
- 实现二叉树的遍历:先序、中序、后序,并理解各自的应用
- 实现二叉树的插入和删除操作 
- 理解平衡二叉树的意义,实现AVL树 
- 理解红黑树的性质和实现,实现简单的红黑树   
- 理解 B 树和 B+ 树,实现 B+ 树 

4. 排序与查找(2周)- 实现冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序 
- 分析各排序算法的时间复杂度和空间复杂度,理解其最坏、最好、平均情况 
- 实现二分查找,分析其时间复杂度 

5. 项目实践(4周)- 实现 LRU 缓存淘汰算法 
- 实现一个简单的词频统计与排名
- 实现搜索引擎的基本功能:爬取网页、构建索引、查询结果排序

6. 扩展学习(至少6周) - 系统学习STL中的数据结构与算法,如 set、map、vector 等 
- 学习算法导论中的高级算法,如最短路径、最小生成树等 
- 学习其他高级数据结构,如线段树、并查集、Trie 树等   
- 不断复习与提高,修正理解的错误和不足

学习数据结构与算法,以下几点方法与鼓励非常重要:

1. 理论与实践结合。只看理论,不编程实现无法真正理解和掌握。要通过实现,编码,练习来巩固理论知识。

2. 阅读推荐书籍。《算法导论》《数据结构与算法 Python 语言描述》等,这些书籍详细而深入,有助更全面掌握知识。

3. 学习官方文档。像 Python 的官方文档对各数据结构与算法都有详细介绍与实现,这也是一个很好的学习资源。

4. 实践通过项目。实现一些实用项目,如 LRU 缓存,使用数据结构与算法知识,极大提高理解和运用能力。

5. 划定学习进度与时间表。制定一份学习计划,比如一周一小结,每天花多长时间学习,避免断断续续,这样可以持之以恒。

6. 边学习边总结。将数据结构与算法理论知识,实现细节与示例代码进行总结,便于复习与 recollection。

7. 不断复习与回顾。利用总结的知识与代码,进行重复的回顾与复习,巩固理解,弥补不足。这是一个永无止境的过程。

8. 多与人交流。将学习遇到的问题、感触与疑问与他人交流,这可以得到新的视角与思路,加速理解。

9. 培养态度。数据结构与算法的学习难度较大,需要耐心与乐观的态度。相信通过不断实践,定会慢慢掌握。

10. 遇到瓶颈再回顾基础。如果在学习或实践中遇到 concept 难以理解的地方,不妨再回顾基础数据结构的相关知识,然后再于深入学习。

综上,学习数据结构与算法最关键是培养良好的学习态度与习惯。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小翟不会写代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值