哈希表(散列表)
2.1 哈希表基本概念
散列存储(哈希存储):将要存储的元素的关键字和存储位置之间建立映射关系,这个映射称为哈希函数。
存储:根据哈希函数直接算出存储位置,存入数据
查找:同样通过哈希函数计算位置,直接访问数据
理想情况下,查找、插入、删除时间复杂度可以达到 **O(1)**。
哈希表核心三要素:哈希函数、存储容器、哈希冲突处理机制。
2.2 哈希冲突
哈希冲突:不同关键字,经过同一个哈希函数计算,得到了相同存储下标。
冲突无法完全避免,我们要做的是设计良好哈希函数 + 高效冲突处理方案,降低冲突带来的性能损耗。
2.3 常见冲突解决思路
- 开放定址法(闭散列)
发生冲突就按照某种规则,在数组内继续向后探测寻找下一个空位,包含线性探测、二次探测、双重散列。所有数据全部存放在数组中,不使用链表。缺点:容易产生数据聚集,删除比较麻烦,不能直接置空。
- 链地址法(拉链法、开散列)
数组每个下标位置保存链表头指针;哈希到同一个下标的全部元素,挂在这个位置对应的单链表上。
优点:实现简单,删除方便,不会干扰其他下标元素;负载因子可以大于1。
缺点:需要额外内存保存链表指针;极端全部冲突会退化成链表O(n)查找效率。
2.4 哈希表特点总结
优点:理想O(1)查找,查询速度极快
缺点:
-
存在哈希冲突,最坏性能退化
-
不支持范围遍历,不适合有序输出
-
需要设计好哈希函数,负载因子过大需要做扩容
工程中C++ unordered_map、Java HashMap底层都是哈希表(链地址法)。
算法基础
3.1 算法五大评价指标
-
正确性:合法输入得到正确输出;非法输入给出合理处理。
-
可读性:代码清晰,方便阅读交流,高内聚低耦合。
-
健壮性:面对非法输入不会直接崩溃,能够异常处理。
-
时间复杂度:衡量算法执行时间随数据规模n增长的趋势。
-
空间复杂度:衡量算法额外开辟的内存空间随n增长的趋势。
3.2 大O表示法(时间复杂度)
大O:只关心数据规模很大时的增长趋势,忽略常数,只保留最高阶项,去掉最高阶系数。
计算规则:
-
常数全部替换为1;
-
只保留表达式最高阶项;
-
去掉最高阶项相乘系数。
常见复杂度从小到大排序
O(1) < O(logn) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
复杂度说明|O(1)
常数时间,与n无关|变量交换,简单运算
O(logn)对数,每次缩小一半范围二分查找
O(n)|线性,遍历全部数据|数组遍历
O(nlogn)线性对数快速排序、希尔排序
O(n²)平方级,双层循环|冒泡、插入、选择排序
示例分析
// O(1) 常数复杂度
void swap(int *a,int *b)
{
int tmp=*a;
*a=*b;
*b=tmp;
}
// O(n) 线性复杂度
void fun1(int n)
{
for(int i=0;i<n;i++){
printf("%d\n",i);
}
}
// O(logn) 对数复杂度,i *=2,每次翻倍
void fun2(int n)
{
for(int i=1;i<n;i*=2){
printf("%d\n",i);
}
}
// O(n²) 双层循环
void fun3(int n)
{
for(int i=0;i<n;i++){
for(int j=i;j<n;j++){
printf("%d %d\n",i,j);
}
}
}
四、经典排序算法(C代码+特性)
4.1 插入排序
思想:把数组分为有序区、无序区;逐个取无序区元素,插入到前面有序区合适位置。
时间复杂度:最坏、平均 O(n²);最好(已有序)O(n)
空间复杂度:O(1),原地排序
稳定性:稳定排序
void insert_sort(int *a, int len)
{
int i,j,tmp;
for(i = 1; i < len; i++)
{
tmp = ai;
j = i;
while(j>0 && tmp < aj-1)
{
aj = aj-1;
j--;
}
aj = tmp;
}
}
4.2 希尔排序(缩小增量排序)
> 插入排序的优化版本,先分组预排序,逐步降低增量,增量最后变为1退化为普通插入排序。
时间复杂度:O(nlogn) ~ O(n²)
空间复杂度:O(1)
稳定性:不稳定排序(相同元素可能交换相对位置)
void shell_sort(int *a, int len)
{
int inc = 0;
int i = 0, j= 0, tmp = 0;
for (inc = len/2; inc > 0; inc /= 2)
{
for (i = inc; i < len; ++i)
{
tmp = ai;
j = i;
while (j >= inc && tmp < ajinc)
{
aj = ajinc;
j -= inc;
}
aj = tmp;
}
}
}
4.3 快速排序(分治思想)
选一个基准key,双指针左右向中间遍历;小于基准放左边,大于基准放右边;递归处理左右子区间。
平均时间复杂度:O(nlogn);最坏退化为O(n²)
空间复杂度:O(logn)(递归栈)
稳定性:不稳定排序
void quick_sort(int *a, int begin, int end)
{
if (begin >= end)
{
return ;
}
int i = begin;
int j = end;
int key = ai;
while (i < j)
{
while (i < j && key <= aj)
{
--j;
}
ai = aj;
while (i < j && key >= ai)
{
++i;
}
aj = ai;
}
ai = key;
quick_sort(a, i+1, end);
quick_sort(a, begin, i1);
}
五、经典查找算法:二分查找
前提条件:数组必须有序(升序/降序)
时间复杂度:O(logn)
空间复杂度:迭代实现O(1);递归O(logn)
//迭代版二分查找,找到返回下标,找不到返回1
int binary_search(int arr\[\],int len,int key)
{
int left=0;
int right = len1;
while(left <= right)
{
int mid = (left+right)/2;
if(arrmid == key)
{
return mid;
}else if(arrmid < key)
{
left = mid+1;
}else{
right = mid1;
}
}
return -1;
}