数据结构与算法
数据结构
是一门研究数据之间关系的学科。
主要有两类需要研究的关系:
物理关系:数据在内存中的实际关系。
顺序结构:根据数据之间的相对位置确定关系
链式结构:在数据中添加一个指针域(地址域),用于指向跟它有关系的数据。
逻辑关系:无视物理关系,人为添加的一种关系。
集合:数据同属一个集体,除此之外没有任何关系。
表:数据之间存在一对一关系,如:数组(顺序表),链表(链式表)。
树:数组之间存在一对多关系。
图:数据之间存在多对多关系。
注意:我们常说的数据结构是逻辑关系,而数据结构的存储方式是物理关系。每种逻辑结构采用什么物理结构存储并没有明确规定,通常以代码实现的难度、以及时间、空间复杂度的要求,选择最合适的物理结构存储,也有可能是链式和顺序的混合储存。
总结
学习数据结构的三个关键点:
1、物理结构
2、逻辑结构
3、结构运算
数据结构常有的运算
创建数据结构:create
销毁数据结构:destroy
清除所有元素:clear
遍历数据结构:show、print
从数据结构中删除一个元素:delete
把一个元素插入到数据结构:install
修改数据结构中某个元素:modify
查询数据结构中的元素:query、find
访问其中一个元素:access
功能被限制的表
栈:只有一个端口进入,元素先进后出FILO。
而栈内存正式使用了这种结构管理内存,所以才叫栈内存。
队列:把一个表结构限制成有两个端口,一个端口只能进,另一个端口只能出,先进先出FIFO。
队头 0
队尾 0
队空 队头 == 队尾
队满 队头 == (队尾+1)%cal;
出队 front = (front+1)%cal;
入队 rear = (rear+1)%cal;
队头元素 ptr[front]
队尾元素 (cal+rear-1)%cal
链式表:
元素:由数据域,指针域组成,元素之间使用指针域连接,这种叫链式存储结构。
如果元素中只有一个指针域,且指向下一个元素,这样元素之间就只存在一对一关系,这种结构叫链式表。
树型结构
树的逻辑结构:数据之间存在一对多关系,常用于表示:家族族谱,组织的架构等。
一般普通树先转换成二叉树(子节点最多有两个的树)再进行讨论。
通常把树的数据项成为节点,子节点的数量叫做度。
struct Node
{
TYPE data;
struct Node* sub;
struct Node* head;
}Node;
二叉树的分类:
普通二叉树:最多有两个子节点,一个叫左子树,一个叫右子树,次序不能颠倒。
满二叉树:二叉树只有度为0的节点和度为2的节点,并且度为0的节点都在最底层,则这棵二叉树为满二叉树。
完全二叉树:深度为h的二叉树,除第h层外,其他各层(1~h-1)的节点数都达到最大个数,第h层所有的节点都连续集中在最左边,这就是完全二叉树。
二叉树的性质:
性质1:二叉树的第i层上至多有2^(i-1)(i>=1)个节点。
性质2:深度为h的二叉树中至多含有2^h-1个节点。
性质3:若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1。
性质4:具有n个节点的完全二叉树深度为log2x+1(其中x表示不大于n的最大整数)。
性质5:若对一棵有n个节点的完全二叉树进行顺序编号(1≤i≤n),那么,对于编号为i(i≥1)的节点:
当i=1时,该节点为根,它无双亲节点。
当i>1时,该节点的双亲节点的编号为i/2。
若2i≤n,则有编号为2i的左节点,否则没有左节点。
若2i+1≤n,则有编号为2i+1的右节点,否则没有右节点。
二叉树的存储:
顺序存储:先补成完全二叉树,然后利用性质5,对二叉树进行操作。
如果数比较稀疏,会非常浪费内存。
链式存储:一个节点由数据、左子树指针、右子树指针组成。
如何表示一棵二叉树:
字符串:把所有节点的左右子树填充为#,然后从上到下描述一棵树
前序中序后序:
前序:根、左、右
中序:左、根、右
后序:左、右、根
注意:前序+中序或 中序+后序可以还原一棵二叉树。
有序二叉树:
左子树 < 根 <= 右子树
特点:可以动态得插入删除节点。
算法
广义:解决特定问题的方法。
狭义:数据结构的运算。
解决问题的方法和步骤(数据结构所具备的功能)
特征:有穷性、确切性、输入项、输出项、可行性
评定:
时间复杂度:随着输入项的变化,算法运算得次数,一般采用大o表示法。
o(1)<o(logn)<o(n)<o(nlogn)<o(n2)<o(2n)<o(n!)
空间复杂度:随着输入项的变化,算法所需要的内存的变量,一般采用大o表示法。
o(1)<o(n)<o(2n)
正确性
可读性
健壮性
排序算法
冒泡排序
特点:对数据的有序性敏感,一旦排序完成就能停止。
时间复杂度:
最优:o(n)
最差:o(n*2)
void bubble_sort(int* arr,int 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;
}
}
}
}
选择排序
特点:交换数据的次数比其他算法少。
时间复杂度:o(n^2)
void select_sort(int* arr,int len)
{
for(int i=0; i<len-1; i++)
{
int min = i;
for(int j=i+1; j<len; j++)
{
if(arr[min] > arr[j])
min = j;
}
if(min!=i)
swap(arr[i],arr[min]);
}
}
插入排序
特点:把数据分成两份,一部分有序,一部分无序,把无序中的数据逐个往有序部分中插入,适合向有序的数据中添加新的数据。
时间复杂度:o(n^2)
void insert_sort(int* arr,int len)
{
for(int i=1,j; i<len; i++)
{
int num = arr[i];
for(j=i; j-1>=0&&num<arr[j-1]; j--)
{
arr[j] = arr[j-1];
}
arr[j] = num;
}
}
希尔排序
特点:是插入排序的改进版,通过缩减增量来提高排序速度的一种方法。
时间复杂度:o(n1.3)~o(n2)
void shell_sort(int* arr,int len)
{
for(int k=len/2; k>0; k=k/2)
{
for(int i=k,j; i<len; i+=k)
{
int num = arr[i];
for(j=i; j-k>=0 && num<arr[j-k]; j-=k)
{
arr[j] = arr[j-k];
}
arr[j] = num;
}
}
}
快速排序
特点:先确定一个标杆,然后从左边找比标杆大的数与标杆交换,再从右边找比标杆小的数与标杆交换,达到标杆左边的数据都比它小,右边的都比它大,达到大致有序的效果,然后再使用同样的方法排序标杆左边的数据,然后再排序标杆右边的数据,直到排序完成。
时间复杂度:o(nlogn),o(n^2)
void _quick(int* arr,int left,int right)
{
// 要求待排序的数据>1
if(left >= right) return;
// 计算标杆
int pi = (left+right)/2;
// 备份标杆的值
int pv = arr[pi];
// 备份左右边界
int l = left,r = right;
while(l < r)
{
// 在标杆的左边寻找比标杆大的值
while(l<pi && arr[l]<=pv) l++;
// 如果寻找到则与标杆交换位置
if(l<pi)
{
arr[pi] = arr[l];
pi = l;
}
// 在标杆的右边寻找比标杆大的值
while(pi<r && arr[r]>=pv) r--;
// 如果寻找到则与标杆交换位置
if(pi<r)
{
arr[pi] = arr[r];
pi = r;
}
}
arr[pi] = pv;
if(pi-left>1) _quick(arr,left,pi-1);
if(right-pi>1) _quick(arr,pi+1,right);
}
void quick_sort(int* arr,int len)
{
_quick(arr,0,len-1);
}
堆排序
特点:把待排序的数据看作一个完全二叉树,先构建成再根堆或小根堆,然后把堆顶与最后一个数据交换,再把待排序的数据-1,然后再调整堆,直到排序完成。
时间复杂度:o(nlogn)
// 循环
void heap_for_sort(int* arr,int len)
{
// 构建堆
for(int i=len-1; i>0; i--)
{
int j = i;
while(j>=0)
{
if(arr[j] > arr[(j+1)/2-1])
{
swap(arr[j],arr[(j+1)/2-1]);
}
j = (j+1)/2-1;
}
}
for(int i=len-1; i>0; i--)
{
swap(arr[0],arr[i]);
int j = 0;
while(j<i)
{
int l = (j+1)*2-1;
int r = (j+1)*2;
if(r<i && arr[l] < arr[r])
{
swap(arr[l],arr[r]);
}
else if(l<i && arr[j] < arr[l])
{
swap(arr[j],arr[l]);
j = l;
}
else
{
break;
}
}
}
}
// 递归
void create_heap(int* arr,int i,int len)
{
if(i >= len)
return;
int l = (i+1)*2-1;
int r = (i+1)*2;
create_heap(arr,l,len);
create_heap(arr,r,len);
if(r < len && arr[l] < arr[r])
swap(arr[l],arr[r]);
if(l < len && arr[i] < arr[l])
swap(arr[i],arr[l]);
}
void heap_sort(int* arr,int len)
{
show_arr("sort befor",arr,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(__func__,arr,len);
}
归并排序
把待排序的数据分隔成一个个子结构,然后把这些子结构按从小到大的顺序复制到临时空间,复制完成后再从临时空间拷贝到原空间,这样就排序完成,该算法使用复制过程代替交换过程,以此提高排序速度,是一种典型的使用空间换取时间算法。
时间复杂度:o(nlogn)
void merge(int* arr,int* tmp,int left,int min,int right)
{
int i = left , l = left , r = min+1;
while(l<=min && r<=right)
{
if(arr[l] < arr[r])
tmp[i++] = arr[l++];
else
tmp[i++] = arr[r++];
}
while(l<=min)tmp[i++] = arr[l++];
while(r<=right) tmp[i++] = arr[r++];
memcpy(arr+left,tmp+left,(right-left+1)*sizeof(arr[0]));
}
void _merge_sort(int* arr,int* tmp,int left,int right)
{
if(left >= right)
return;
int min = (left+right)/2;
_merge_sort(arr,tmp,left,min);
_merge_sort(arr,tmp,min+1,right);
merge(arr,tmp,left,min,right);
}
void merge_sort(int* arr,int len)
{
int* tmp = malloc(sizeof(int)*len);
_merge_sort(arr,tmp,0,len-1);
free(tmp);
}
计数排序
把待排序的数据中的最大值、最小值,然后创建哈希表,统计每个数据出现的次数,然后按从小到大的顺序访问哈希表,当哈希表中的数据不为零说明该数据现为待排序的数据,然后把它存放到数组中。
缺点:创建哈希表需要额外的内存,数据的最大值、最小值不确定可能效率非常低。(只适合对数据之间差值不打,重复性高的数据进行排序)。
时间复杂度:o(n+k)
void count_sort(int* arr,int len)
{
int max = arr[0] , min = arr[0];
for(int i=1; i<len; i++)
{
if(arr[i] > max)
max = arr[i];
if(arr[i] < min)
min = arr[i];
}
int* cntp = calloc(4,max-min+1);
for(int i=0; i<len; i++)
{
cntp[arr[i]-min]++;
}
int index = 0;
for(int i=min; i<=max; i++)
{
for(int j=0; j<cntp[i-min]; j++)
{
arr[index++] = i;
}
}
}
桶排序
先按照数据的规模创建桶,然后再把数据分散到桶中,然后调用其他排序算法把桶进行排序,然后再把桶中的数据合并到数组中,该算法是通过降低待排序数据的规模来提高排序速度。
缺点:如何分桶,桶定义多大,这些都需要对有数据有大致的了解。
时间复杂度:o(n+k)
基数排序
创建10个队列,把待排序的数据逐个识别个位数据,然后根据识别的结构吧数据存储到10个队列中,然后从队列出一次弹出,然后是十位、百位…重复这一过程,直到所有数据的最高位为0。
缺点:只适合排序正整数据。
时间复杂度:o(n+k)
统计十种排序算法 最优 最差 平均 是否稳定
查找算法
顺序查找
逐个遍历对比,对数据没有要求。
时间复杂度:o(n)
二分查找
要求数据必须有序,根据左右便捷计算出中间值,如果待查找的数据比中间值要小则在中间值的左边继续查,否则在右继续查,重复以上步骤,知道找到或者边界相遇。
时间复杂度:o(longn)
哈希查找
需要先对数据建立哈希表,然后再访问哈希表查询。
时间复杂度:o(1)~o(n)
块查找
需要先对数据进行分块,查找时先找到对应的数据块,再按照顺序或二分查找.
时间复杂度:o(1)~o(n)