数据结构与算法
算法
- 数据结构中的算法,指的是数据结构所具备的功能
- 解决特定问题的方法,它是前辈们的一些优秀的经验总结
算法五大特征
- 有穷性、可行性、确定性、输入、输出
如何评价一个算法
时间复杂度
- 由于计算机性能的不同,无法准确地统计出算法执行所需要的时间
- 因此采用算法的执行次数来代表算法的时间复杂度O(n的公式) 忽略常数
常见的时间复杂度
-
// O(1) printf("%d",i); // O(n) for(int i=0; i<n; i++) { printf("%d",i); } // O(logn) // 以2为底的log,使用n或N for(int i=n; i>=0; i/=2) { printf("%d",i); } // O(nlogn) for(int i=0; i<n; i++) { for(int j=n; j>=0; j/=2) { printf("%d",i); } } // O(n^2) for(int i=0; i<n; i++) { for(int j=0; j<n; j++) { printf("%d",i); } }
空间复杂度
-
执行一个程序所需要的额外的内存空间大小,是对一个算法在运行过程中额外临时占用的空间大小的一个度量
-
通常来说,只要算法不涉及动态分配的空间以及递归,基本上空间复杂度O(1)
分治
- 分而治之
- 把一个大而复杂的问题,分解成若干个小而简单的问题,利用计算机强大的计算能力,来解决所有的小问题,是一种解决问题的算法思想
- 实现分治思想的方法
- 循环、递归
查找
顺序查找
- 对待查找的数据没有要求,从头到尾逐一比较,在小规模数据查找中比较常见,相比较而言效率较低
- 时间复杂度:O(n)
// 顺序查找
int order_find(int* arr,int len,int key)
{
for(int i=0; i<len; i++)
{
if(arr[i] == key) return i;
}
return -1;
}
二分查找(折半查找)
-
待查找数据必须有序,从中间位置数据开始比较查找,如果中间值比key小,则从左边部分进行二分查找,反之比key大,则从右边部分进行二分查找,如果相同则查找成功,当查找的范围缩小为0,则查找失败
-
时间复杂度:O(logn)
// 循环二分
int binary_for_find(int* arr,int len,int key)
{
int l = 0,r = len-1;
while(l <= r)
{
int m = (l+r)/2;
if(arr[m] == key) return m;
if(arr[m] > key) r = m-1;
else l = m+1;
}
return -1;
}
// 递归二分
int _binary_find(int* arr,int l,int r,int key)
{
if(l>r) return -1;
int m = (l+r)/2;
if(arr[m] == key) return m;
if(arr[m]<key)
return _binary_find(arr,m+1,r,key);
if(arr[m]>key)
return _binary_find(arr,l,m-1,key);
}
int binary_recusion_find(int* arr,int len,int key)
{
return _binary_find(arr,0,len-1,key);
}
块查找
- 是一种数据查找处理的思想,不是特定的算法,当数据量过多,可以把数据进行特定的分块处理,然后再进行查找
- 例如单词字典
哈希查找Hash
- 数据先经过哈希函数计算出该数据在哈希表中的位置,然后标记该位置的数据,之后查找数据直接从哈希表中查找,它的查找时间复杂度最高能到 O(1)
- 但是该算法有很大局限性,不适合浮点型、有符号负数数据,还需要额外的内存空间,空间复杂度很高,还不好确定额外的空间大小
- 是一种典型的空间换时间的算法
- 设计哈希函数的方法:
- 1、直接定址法
- 把数据当做哈希表的下标来标记哈希表
- 2、数据分析法
- 先分析数据的特点然后设计哈希函数,常用方法是先找出最大、最小值
- 哈希表长度:最大-最小+1
- 哈希函数:数据-最小值 作为哈希表下标进行标记
- 平方折中法、折叠法、随机数法等方法设计哈希函数
- 如果出现相同数据时,哈希表会出现所谓的哈希冲突,一般使用链表解决
- hash函数应用:MD-5、SHA-1都属于Hash算法
- 1、直接定址法
// 哈希查找
bool hash_find(int* arr,int len,int key)
{
/*
// 1.直接定址法
// 创建哈希表
int hash[100000] = {};
// 标记
for(int i=0; i<len; i++)
{
hash[arr[i]]++;
}
// 查找
return hash[key];
*/
// 2.数据分析法
int min = arr[0], max = arr[0];
for(int i=0; i<len; i++)
{
if(arr[i] > max) max = arr[i];
if(arr[i] < min) min = arr[i];
}
// 创建哈希表
int hash[max-min+1];
memset(hash,0,4*(max-min+1));
// 标记
for(int i=0; i<len; i++)
{
hash[arr[i]-min]++;
}
if(key < min || key > max) return false;
// 查找
return hash[key-min];
}
排序
排序算法的稳定性
- 待排序数据中,当存在值相同的数据,如果在排序的过程中,不会改变它们的前后顺序,则认为该排序算法是稳定的
冒泡排序
-
数据左右相比,把大的数据交换到右边,然后继续下两个数据左右相比,直到最大的数据交换到末尾,重复以上步骤
-
注意:在冒泡排序中一旦发现数据已经有序,立即停止排序,称为有序性敏感
-
适合数据基本有序的情况,效率非常高
-
时间复杂度:最好情况下是 O(n),平均上是 O(n^2)
-
稳定性:稳定
-
// 冒泡 void bubble_sort(TYPE* arr,size_t len) { bool flag = true; // 解决有序性敏感 for(int i=len-1; i>0 && flag; i--) { flag = false; for(int j=0; j<i; j++) { if(arr[j] > arr[j+1]) { flag = true; SWAP(arr[j],arr[j+1]); } } } }
选择排序
-
假定最开始的位置是最小的并记录下标min,然后与后面的数据进行比较,如果有比min为下标的数据更小的值,则更新min,如果比较结束后,min的位置发生了改变,则交换最开始的位置与min位置的值
-
选择排序相当于冒泡排序的一种变种,但是不存在有序性敏感,比较适合数据比较混乱的情况,比冒泡快
-
时间复杂度:O(n^2)
-
虽然时间复杂度没有变化,但是数据的交换次数比较少,因此实际的运行速度并不慢(数据交换要比赋值、数据比较要耗时)
-
稳定性:不稳定
-
10 10 1
-
a b
-
// 选择 void select_sort(TYPE* arr,size_t len) { for(int i=0; i<len-1; i++) { int min = i; for(int j=i+1; j<len; j++) { if(arr[j] < arr[min]) min = j; } if(min!=i) SWAP(arr[min],arr[i]); } }
插入排序
-
把数据看成两部分,一部分是有序的,后一部分的数据与有序部分的数据逐个比较,直到找到合适的位置后,插入到该位置
-
适合对已排序的数据新增数据后进行排序
-
时间复杂度:O(n^2)
-
稳定性:稳定
-
// 插入 void insert_sort(TYPE* arr,size_t len) { // i 是无序部分第一个位置 for(int i=1,j=0; i<len; i++) { int val = arr[i]; for(j=i; j>0 && arr[j-1]>val; j--) { arr[j] = arr[j-1]; } if(j!=i) arr[j] = val; } }
希尔排序(缩小增量排序)
-
是插入排序的增强版,由于插入排序数据移动的速度比较慢,如果待排序数据很混乱,效率比较低,因此引入增量的概念,数据间隔增量个数进行插入排序然后不停地缩小一倍增量,直到增量为1,此时数据从很混乱变为接近有序,以此提高排序的效率
-
5 4 3 2 1 比插入快
-
时间复杂度:O(nlogn)
-
稳定性:不稳定
-
// 希尔 void shell_sort(TYPE* arr,size_t len) { for(int k=len/2; k>0; k/=2) { for(int i=k,j=0; i<len; i++) { int val = arr[i]; for(j=i; j-k>=0 && arr[j-k] >val; j-=k) { arr[j] = arr[j-k]; } if(j!=i) arr[j] = val; } } }
快速排序
-
在中间位置确定为标杆p,备份标杆的数据为val,从左边找比val大的数据,找到后位置为l,把该数据赋值给p位置,更新p为l,然后从右边比val小的数据,找到后位置为r,然后把该数据赋值给p位置,更新p为r,重复以上过程,直到l和r相遇时,结束此次排序,最后把val还原给p位置,最终p左边都比val小,右边都比val大,局部有序,继续对左部分快排,右部分快排,直到全部有序为止
-
注意:快排的综合性能是最高的,因此称为快速排序,也是笔试最常考的排序算法
-
时间复杂度:最好O(n),最坏O(n^2),平均O(nlogn)
-
稳定性:不稳定
-
// 快速 void _quick_sort(TYPE* arr,int left,int right) { if(left >= right) return; // 记录左右标杆 int l = left, r = right; // 找到中间位置标杆 int p = (l+r)/2; // 备份标杆的值 int val = arr[p]; while(l < r) { // 从p左边找比val大的值 while(l < p && arr[l]<=val) l++; // 如果找到了 if(l < p) { arr[p] = arr[l]; p = l; } // 从p的右边找比val小的值 while(r > p && arr[r] >= val) r--; // 如果找到了 if(r > p) { arr[p] = arr[r]; p = r; } } // 还原标杆的值 arr[p] = val; _quick_sort(arr,left,p-1); _quick_sort(arr,p+1,right); } void quick_sort(TYPE* arr,size_t len) { _quick_sort(arr,0,len-1); }
归并排序
-
先将数据拆分成单独的个体,然后再按照从小到大的顺序把两组数据合并到临时空间中,合并结束后,在重新拷回原空间的原位置中,得到数量翻倍的一组组有序的数据,继续两组两组合并,直到整体有序为止
-
归并排序利用了额外的内存空间,避免了数据交换的耗时,是一种典型的以空间换时间的算法
-
时间复杂度:O(nlogn)
-
稳定性:稳定
-
// 归并 void _merge_sort(TYPE* arr,TYPE* temp,int l,int r) { if(l >= r) return; int m = (l+r)/2; _merge_sort(arr,temp,l,m); _merge_sort(arr,temp,m+1,r); // 最后合并两部分 // 前提:左右两部分各自有序 // 左:最小l 最大m // 右:最小m+1 最大r if(arr[m] < arr[m+1]) return; int k = l, i = l, j = m+1; while(i<=m && j<=r) { if(arr[i] < arr[j]) temp[k++] = arr[i++]; else temp[k++] = arr[j++]; } // 把剩余没比完的直接放入temp末尾 while(i<=m) temp[k++] = arr[i++]; while(j<=r) temp[k++] = arr[j++]; // 把数据从temp还原回arr中 memcpy(arr+l,temp+l,sizeof(TYPE)*(r-l+1)); } void merge_sort(TYPE* arr,size_t len) { // 先申请临时空间 TYPE* temp = malloc(sizeof(TYPE)*len); _merge_sort(arr,temp,0,len-1); // 释放临时空间 free(temp); }
堆排序
-
先把数据看成一棵完全二叉树,然后先调整成大顶堆或小顶堆,然后把根节点与末尾交换,然后有效个数-1,重新调整成堆结构,继续以上步骤,直到有效个数为1,此时整个数组就有序了,既可以循环也可以递归实现
- 大顶堆:堆排序后 从小到大
- 小顶堆:堆排序后 从大到小
-
时间复杂度:O(nlogn)
-
稳定性:不稳定
-
// 堆排序递归实现 // 从top下标 到end下标 向下调整成堆结构 void _sort_heap(int* arr,int top,int end) { if(top >= end) return; int max = top+1; // 编号 int l = max*2; // 左子树编号 int r = max*2+1; // 右子树编号 if(l-1 <= end && arr[l-1] > arr[max-1]) // 有左 { max = l; } if(r-1 <= end && arr[r-1] > arr[max-1]) // 有右 { max = r; } if(max != top+1) { // 值最大的不是根,则交换 SWAP(arr[max-1],arr[top]); _sort_heap(arr,max-1,end); } } void sort_heap_recursion(int* arr,int len) { // 把数组调整成大顶堆结构 for(int i=1; i<len; i++) { int index = i+1; //编号 while(index > 1) { if(arr[index-1] > arr[index/2-1]) { SWAP(arr[index-1],arr[index/2-1]); index = index/2; } else break; } } for(int i=len-1; i>0; i--) { SWAP(arr[0],arr[i]); _sort_heap(arr,0,i-1); } }
计数排序
-
在数据中找出最大值和最小值,通过 最大值-最小值 方式创建哈希表,然后根据 数据-最小值 的值作为哈希表的下标,访问并标记(累加)对应的哈希表,然后标记所有数据
-
排序时,遍历整个哈希表,如果表中数据非0,把该位置下标+最小值 依次存回排序数组中,遍历结束数组有序
-
理论上计数排序非常快,因为它不是基于比较的排序,在一定范围内整数排序时快于任何一种比较排序算法,但是有很大的局限性,只适合无符号整型、正整型数据,并且数据的差值不宜过大,否则会非常浪费内存,因此如果数据越平均,重复数据多的情况,该排序性价比越高
-
空间换时间
-
时间复杂度:O(n+k)(k是待排序的整数的范围)
- 如果k范围过大,效率可能会低于O(nlogn)
-
稳定性:稳定
-
// 计数 void count_sort(TYPE* arr,size_t len) { int min = arr[0], max = arr[0]; for(int i=0; i<len; i++) { if(min > arr[i]) min = arr[i]; if(max < arr[i]) max = arr[i]; } // 创建哈希表 int* hash = calloc(max-min+1,4); // 标记哈希表 for(int i=0; i<len; i++) { hash[arr[i]-min]++; // 必须累加 } // 遍历哈希表,还原排序后的数据 for(int i=0,j=0; i<max-min+1; i++) { while(hash[i]--) { arr[j++] = i+min; } } // 销毁 free(hash); }
桶排序
-
根据数据的值,存储到不同的桶中,然后调用其他的排序算法,对桶中的数据进行排序,然后再全部按桶的顺序拷贝回数组中,以此降低排序的规模来提高排序速度,以空间换时间的算法
-
缺点:如何分桶、桶有几个、桶定义多大都不一定,需要对排序数据有一定的了解和分析后才能确定
-
时间复杂度:O(n)
-
稳定性:稳定
-
// 桶 // cnt桶数 range 桶中数据的范围 void _bucket_sort(TYPE* arr,size_t len,int cnt,int range) { // 申请桶的内存 bucket桶的首地址 bucket_end指向桶中最后一个元素的地址 TYPE* bucket[cnt],*bucket_end[cnt]; for(int i=0; i<cnt; i++) { // 最坏情况 所有数据在一个桶中 bucket[i] = malloc(sizeof(TYPE)*len); // 开始时,起始,末尾指针都指向开头 bucket_end[i] = bucket[i]; } // 把所有数据,按照桶的范围值放入对应的桶中 for(int i=0; i<len; i++) { for(int j=0; j<cnt; j++) { if(range*j <= arr[i] && arr[i] < range*(j+1)) { *(bucket_end[j]) = arr[i]; bucket_end[j]++; // 末尾指针往后移动一个元素 break; } } } // 调用其他的排序算法,对每个桶中的数据排序 for(int i=0; i<cnt; i++) { // 计算桶中元素个数 int size = bucket_end[i] - bucket[i]; if(0 < size) count_sort(bucket[i],size); // 把排序好的数据依次还原回数组中 memcpy(arr,bucket[i],sizeof(TYPE)*size); arr += size; free(bucket[i]); } } void bucket_sort(TYPE* arr,size_t len) { _bucket_sort(arr,len,4,25); show_arr(arr,len); printf(" %s\n",__func__); }
基数排序
-
是桶排序思想的其中一种具体实现,首先创建10个队列(桶),然后依次按照数据的个、十、百位…的顺序,入队到对应的桶中,然后依次每个出队回原来的数组中,当最高位都处理完且返回数组后,数组就有序了
-
缺点:只适合排序正整数数据,需要准备大量的临时空间
-
时间复杂度:O(n)
-
稳定性:稳定
-
// 基数 void radix_sort(TYPE* arr,size_t len) { // 创建队列 ListQueue* queue[10] = {}; for(int i=0; i<10; i++) { queue[i] = create_list_queue(); } // 循环此数由最大值的位数决定 int max = arr[0]; for(int i=1; i<len; i++) { if(arr[i] > max) max = arr[i]; } // i 1个位 2十位...... for(int k=1; max/k>0; k*=10) { for(int j=0; j<len; j++) { // 获取每个数的每个位的值 int index = arr[j]/k%10; push_list_queue(queue[index],arr[j]); } // 依次从队列重新返回到数组 int index = 0; for(int j=0; j<10; j++) { while(!empty_list_queue(queue[j])) { arr[index++] = head_list_queue(queue[j]); pop_list_queue(queue[j]); } } } for(int i=0; i<10; i++) { destroy_list_queue(queue[i]); } }