算法:
数据结构中的算法,指的是数据结构所具备的功能
解决特定问题的方法,它是前辈们的一些优秀的经验总结
输入:一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
确定性:算法中的每一条指令必须有确切的含义,不能产生多义性:
可行性:算法中的每一条指令必须是切实可执行的,即原则上可以通过已经实现的基本运算执行有限次来实现(也称之为有效性);
有穷性:算法必须能在有限步后终止;
如何评价一个算法:
时间复杂度:
由于计算机的性能不同,无法准确地计算出算法执行所需要的时间
因此我们用算法执行的次数来代表算法的时间复杂度 O(公式) 一般忽略常量
常见的时间复杂度:
// O(1)
printf("%d");
// O(logn)
for(int i=n; i>=0; i=i/2)
{
printf("%d");
}
// O(n)
for(int i=0; i<n; i++)
{
printf("%d");
}
// O(nlogn)
for(int i=0; i<n; i++)
{
for(int j=n; j>=0; j/=2)
{
printf("d");
}
}
// O(n^2)
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
{
printf("%d");
}
}
分治:
把一个大而复杂的问题,分解成很多相同的小而简单的问题,利用计算机强大的计算能力来解决一 个大问题
实现分治的方法:递归、循环
查找算法:
顺序查找:
对待查找的数据没有要求,从头到尾逐一比较,在小规模的数据查找中比较常见,效率比较低。
时间复杂度:O(n)
二分查找:
对待查找的数据必须有序,从数据中间开始比较查找
块查找:
是一种数据处理的思想,而不是一种特定的算法,适合数据量非常多时,可以先把数据进行分块处理,然后再进行分块查找
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 顺序查找
int order_search(int* arr,int len,int key)
{
for(int i=0; i<len; i++)
{
if(arr[i] == key)
return i;
}
return -1;
}
//排序
void sort(int* arr,int len)
{
for(int i=0; i<len-1; i++)
{
for(int j=i+1; j<len; j++)
{
if(arr[j] < arr[i])
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
// 循环二分
int bin_search_for(int* arr,int len,int key)
{
int l = 0, r = len-1;
while(l <= r)
{
int p = (l+r)/2;
if(arr[p] == key) return p;
if(key < arr[p])
r = p-1;
else
l = p+1;
}
return -1;
}
int _bin_search(int* arr,int l,int r,int key)
{
if(l > r) return -1;
int p = (l+r)/2;
if(arr[p] == key)
return p;
if(key < arr[p])
return _bin_search(arr,l,p-1,key);
else
return _bin_search(arr,p+1,r,key);
}
// 递归二分
int bin_search(int* arr,int len,int key)
{
return _bin_search(arr,0,len-1,key);
}
int main(int argc,const char* argv[])
{
int arr[10] = {};
for(int i=0; i<10; i++)
{
arr[i] = rand()%100;
printf("%d ",arr[i]);
}
printf("\nindex:%d\n",order_search(arr,10,150));
sort(arr,10);
for(int i=0; i<10; i++)
{
printf("%d ",arr[i]);
}
printf("\nindex:%d\n",bin_search_for(arr,10,93));
}
排序算法:
排序算法的稳定性:
在待排序的数据中,如果有相同的数据,排序过程中不会改变它们的先后顺序,则认为该排序算法是稳定的
冒泡:
数据左右进行比较,把最大的数据交换到最后,特点是该算法对数据的有序性敏感,在排序的过程中发现有序可以立即停止排序。
如果待排序的数据基本有序,则冒泡排序的效率是非常高的
时间复杂度: 最好O(n) 平均O(n^2)
稳定的
选择:
假定最开始的位置是最小值并记录下标min,然后与后面的数据进行比较,如果有比min为下标的数据小的
则更新min,最后如果min的位置与开始位置发生了改变,则交换min位置的数据与开始位置的数据,
以此类推。
虽然时间复杂度挺高的,但是数据交换的次数比较少,因此实际运行时间并不慢(交换比比较耗时)
时间复杂度: O(n^2)
不稳定 10 10 1
插入:
把数据看作两部分,一部分是有序,把剩余的数据逐个插入进去。
适合对已经排序后的数据,新增数据后进行排序
时间复杂度: O(n^2)
稳定的
希尔:
是插入排序的增强版,由于插入排序时,数据的移动速度比较慢,所以增加了增量的概念,以此来提高
排序速度
时间复杂度:O(n^2)
不稳定
快速:
找到一个标杆,一方面从左边找比标杆大的数据,找到后把数据放在标杆右边,另一方面从
右边找比标杆小的数据,找到后放在标杆的左边,最终标杆左边的数据比右边的小,这次就两部分有序,
然后用同样的方法排序标杆左边和右边的数据
特点:综合性最高,因此叫快速排序,笔试面试考得最多的排序算法
时间复杂度:O(nlogn)
不稳定
归并:
把一组数据拆分成单独的个体,然后按照从小到大的顺序复制到临时空间,复制完成后再从临时空间拷贝
到原内存中。
特点:由于需要使用额外内存空间来避免数据交换的耗时,是一种典型的以空间换时间的排序算法
时间复杂度: O(nlogn)
稳定的
堆:
先把数据当做完全二叉树,然后把树调整成大根树,然后把根节点交换到数组最后,然后数量--,
然后再调整成大根树,以此类推,直到数量为1时结束,可以递归、也可以循环实现
时间复杂度:O(nlogn)
不稳定的
计数:
找出数据中的最大值、最小值,并创建哈希表,把数据-最小值作为哈希表数组的下标来访问哈希表并+1,
然后遍历哈希表,如果数据大于0时,则把下标+最小值还原回数据,依次放回原数组中,达到排序的目的。
该排序算法理论上速度是非常快,但是有很大的局限性,适合整型数据,而且数据的差值不宜过大,否则
会非常浪费内存,也是以空间换时间的排序算法
时间复杂度:O(n+k) k是整数的范围
稳定的
桶:
把数据根据值来存储到不同的桶,然后再调用其它的排序算法,对桶中的数据进行排序,然后再拷贝回数组
中,以此达到降低排序规模来减少排序的时间,也是以空间换时间的方式。
缺点:如何分桶,桶定义多大
时间复杂度:O(n+k)
稳定的
基数:
是桶排序的具体实现,首先创建10个队列,然后逆序计算出数据的个十百千...然后分别压入对应的队列中
,然后从队列中弹出存储的数据到数组中。
缺点:只适合排序正整数,需要准备很大的空间,也是空间换时间
时间复杂度:O (nlog(r)m),其中r为所采取的基数,而m为堆数
稳定的
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define LEN 10
#define swap(a,b) {typeof(a) t=(a);(a)=(b);(b)=t;}
void show_arr(int* arr,size_t len)
{
for(int i=0; i<len; printf("%02d ",arr[i++]));
printf("\n");
}
typedef void (*SortFP)(int*,size_t);//函数指针,具体指向哪个不知道,但是可以指向所有的排序算法
// 冒泡排序
void bubble_sort(int* 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])
{
swap(arr[j],arr[j+1]);
flag = true;
}
}
}
show_arr(arr,len);
printf("%s\n",__func__);
}
// 选择排序
void select_sort(int* 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[i],arr[min]);
}
show_arr(arr,len);
printf("%s\n",__func__);
}
// 插入排序
void insert_sort(int* arr,size_t len)
{
for(int i=1,j; 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;
}
show_arr(arr,len);
printf("%s\n",__func__);
}
// 希尔排序 0 1 2 3 4 5
void shell_sort(int* arr,size_t len)
{
for(int k=len/2; k>0; k/=2)
{
for(int i=k,j; 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;
}
}
show_arr(arr,len);
printf("%s\n",__func__);
}
void _quick_sort(int* arr,int left,int right)
{
if(left >= right) return;
// 计算标杆的下标
int pi = (left+right)/2;
// 备份标杆的数据
int pv = arr[pi];
// 备份左右下标
int l = left,r = right;
for(int i=left; i<=right; i++)
{
if(i==pi) printf("[%d] ",arr[i]);
else printf("%d ",arr[i]);
}
printf("\n");
while(l < r)
{
// 在标杆左边找比它大的数据
while(l < pi && arr[l] <= pv) l++;
if(l < pi)
{
arr[pi] = arr[l];
pi = l;
}
// 在标杆右边找比它小的数据
while(r > pi && arr[r] >= pv) r--;
if(r > pi)
{
arr[pi] = arr[r];
pi = r;
}
}
// 还原标杆的数据
arr[pi] = pv;
// show_arr(arr,LEN);
if(pi-left>1) _quick_sort(arr,left,pi-1);
if(right-pi>1) _quick_sort(arr,pi+1,right);
}
// 快速排序
void quick_sort(int* arr,size_t len)
{
_quick_sort(arr,0,len-1);
show_arr(arr,len);
printf("%s\n",__func__);
}
void merge(int* arr,int* tmp,int l,int p,int r)
{
// l是左部分最小,p是左部分最大 p+1是右部分最小,r是右部分最大
// 左右部分各自有序
if(arr[p] < arr[p+1]) return;
int i=l,j=p+1,k=l;
while(i<=p && j<=r)
{
// 左右部分从左开始比较,谁小谁放入tmp
if(arr[i] < arr[j])
tmp[k++] = arr[i++];
else
tmp[k++] = arr[j++];
}
// 其中一部分比较完成,另一部分全部放入tmp末尾
while(i<=p) tmp[k++] = arr[i++];
while(j<=r) tmp[k++] = arr[j++];
memcpy(arr+l,tmp+l,4*(r-l+1));//把tmp拷贝到arr
}
void _merge_sort(int*arr,int* tmp,int l,int r)
{
// 拆分
if(l >= r) return;
int p = (l+r)/2;
_merge_sort(arr,tmp,l,p);
_merge_sort(arr,tmp,p+1,r);
// 合并
merge(arr,tmp,l,p,r); //l~p p+1~r
}
// 归并排序
void merge_sort(int* arr,size_t len)
{
// 申请临时空间
int* tmp = malloc(4*len);
_merge_sort(arr,tmp,0,len-1);
show_arr(arr,len);
printf("%s\n",__func__);
free(tmp);
}
void create_heap(int* arr,int root,size_t len)
{
// root是根节点的下标
if(root >= len) return;
// 左右子树下标
int left = root*2+1; //(root+1)*2-1,root=0
int right = root*2+2; //(root+1)*2+1-1
// 确保左右子树为左右部分的最大值(怎么确保)
create_heap(arr,left,len);
create_heap(arr,right,len);
// 右子树存在,并且右子树大于左子树,则交换
if(right < len && arr[right] > arr[left])
swap(arr[right],arr[left]);
// 左子树存在,并且左子树大于根,则交换
if(left < len && arr[left] > arr[root])
swap(arr[left],arr[root]);
}
// 堆排序
void heap_sort(int* arr,size_t len)
{
// 调整成大顶堆
create_heap(arr,0,len);
for(int i=len-1; i>0; i--)
{
// 交换根节点与末尾节点
swap(arr[0],arr[i]);
// 重新调整成大顶堆
create_heap(arr,0,i);
}
show_arr(arr,len);
printf("%s\n",__func__);
}
void heap_sort_for(int* arr,size_t len)
{
// 从下往上调整成大顶堆
for(int i=len-1; i>0; i--)
{
int p = (i+1)/2-1;
if(arr[p] < arr[i])
swap(arr[p],arr[i]);
}
for(int i=len-1; i>0; i--)
{
swap(arr[0],arr[i]);
// 重新调整
for(int j=0; j<i; j++)
{
if(j*2+2 < i && arr[j*2+2] > arr[j*2+1])
swap(arr[j*2+2],arr[j*2+1]);
if(j*2+1 < i && arr[j*2+1] > arr[j])
swap(arr[j*2+1],arr[j]);
}
}
show_arr(arr,len);
printf("%s\n",__func__);
}
// 计数排序
void count_sort(int* arr,size_t len)
{
int min = arr[0],max = arr[len-1];
for(int i=0; i<len; i++)
{
if(arr[i] > max) max = arr[i];
if(arr[i] < min) min = arr[i];
}
// 创建哈希表
int* tmp = calloc(4,max-min+1);
for(int i=0; i<len; i++)
{
tmp[arr[i]-min]++;
}
for(int i=0,j=0; i<max-min+1; i++)
{
while(tmp[i]--)
{
arr[j++] = i+min;
}
}
free(tmp);
show_arr(arr,len);
printf("%s\n",__func__);
}
int main(int argc,const char* argv[])
{
int arr[LEN] = {};
SortFP sort[] = {bubble_sort,select_sort,insert_sort,shell_sort,quick_sort,merge_sort,heap_sort,heap_sort_for,count_sort};
for(int i=0; i<sizeof(sort)/sizeof(sort[0]); i++)
{
for(int j=0; j<LEN; j++)
{
arr[j] = rand()%100;
}
show_arr(arr,LEN);
sort[i](arr,LEN);//
}
}
统计十种排序算法:
排序 最优 最差 平均 特点 是否稳定 空间复杂度