算法:
数据结构中的算法,指的是数据结构所具备的功能
解决特定问题的方法,它是前辈们的一些优秀的经验总结
一个算法应该具有以下五个重要的特征:
有穷性:算法的有穷性是指算法必须能在执行有限个步骤之后终止;
确切性:算法的每一步骤必须有确切的定义;
输入项:一个算法有0个或多个输入;
输出项:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
可行性:算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成(也称之为有效性)
如何评价一个算法:
时间复杂度:由于计算机的性能不同,无法准确地衡量出算法执行所需要的时间
因此我们用算法的执行次数来代表算法的时间复杂度
一般使用 O(公式) 一般忽略常数
常见的时间复杂度:
// O(1)
printf("%d",i);
// O(logn)
for(int i=n; i>=0; i/=2)
{
printf("%d",i);
}
// O(n) O(N)
for(int i=0; i<n; i++)
{
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)
例如:求第n个斐波那契数列的递归实现算法 空间复杂度O(n)
注意:对于一个算法而言,其时间复杂度与空间复杂度往往是相互影响的,没有唯一的标准,需要结合实际综合考虑
分治:
分而治之,把一个大而复杂的问题,分解成很多小而简单的问题,利用计算机强大的计算能力来解决问题
实现分治的方法:循环、递归
查找算法:
顺序查找
对待查找的数据没有要求,从头到尾逐一比较,在小规模的查找中较为常见,查找效率较低
时间复杂度:O(N)
二分查找(折半查找)
待查找的数据必须有序,从数据中间位置开始比较查找,如果中间值比key小,则从左边继续进行二分查找,反之从右边进行。
时间复杂度:O(logN)
块查找(权重查找)
是一种数据处理的思想,不是一种特定的算法,当数据量非常多时,可以先把数据进行分块处理,然后再根据分块的条件进行查找,例如英文字典
哈希查找(Hash)
数据 经过 哈希函数 计算出数据在哈希表中的位置,然后标记位置,方便之后的查找,它的时间复杂度最高可以达到 O(1)
但是该算法有很大的局限性,不适合负数、浮点型数据、字符型数据的查找,还需要额外申请存储空间,空间复杂度高,是一种典型的以空间换时间的算法
哈希函数设计方法:
直接定址法:直接把数据当做哈希表的下标,把哈希表中该下标的位置+1
数据分析法:分析数据的特点来设计哈希函数,常用的方法是找到最大值和最小值,用 最大值-最小值+1 确定哈希表的长度,使用 数据-最小值 作为哈希表的下标访问哈希表
平方取中法、折叠法、随机数法,但都无法保证哈希数据的唯一性,出现所谓的哈希冲突,一般使用链表解决
Hash函数的应用:MD5、SHA-1都属于Hash算法中的应用
排序算法:
排序算法的稳定性:
在待排序的数据中,如果有值相同的数据,在排序的全程中都不会改变它们的先后顺序,则认为该排序算法是稳定的
冒泡:数据左右进行比较,把最大的数一直交换到最后,特点是该算法对数据的有序性敏感,在排序过程中发现有序可以立即停止排序,如果待排序的数据基本有序,则冒泡的效率非常高
时间复杂度:最优O(N) 平均:O(N^2)
稳定的
选择:假定最开始的位置是最小值并记录下标min,然后与后面的数据比较,如果有比min为下标的数据还要小,则更新min,最后判断如果min的值发生了改变,则交换min位置的数据与最开始位置的数据
虽然选择排序的时间复杂度较高,但是数据交换次数少,因此实际运行速度并不慢
是冒泡排序的变种,但是没有对数据有序性敏感,数据混乱情况下比冒泡快
时间复杂度:O(N^2)
不稳定的 (10 10 1)
注意:算法的时间复杂度并不能代表算法的实际时间,有时候时间复杂度高的反而速度更快
插入:把数据看作两个部分,一部分是有序的,把剩余的数据逐个插入进去
时间复杂度:O(N^2)
稳定的
希尔:是插入排序的增强版,由于插入排序数据移动的速度比较慢,所以在此基础上增加了增量的概念,从而提高排序的速度
时间复杂度:O(N^(1.3~2))
不稳定
快速:
找到一个标杆p,备份标杆p的值val,一面从左找比val大的数据,找到后赋值给p,更新标杆p的位置到左标杆,然后从右边找比val小的数,找到后也赋值给p,同样更新p到右标杆,反复执行直到左右标杆相与停止,最后把val赋值回p的位置,最终会形成p左边的数都比它小,右边的数都比它大;然后再按照同样的方式对左右两边进行快排,最后全部有序
快速排序的综合性能最高,因此叫做快速排序,笔试面试考最多
时间复杂度:O(NlogN)
不稳定
归并:
先把一组待排序的数据拆分成单独的个体,存放到临时空间中,然后两两比较合并,全部合并完成后再从临时空间中拷贝给原内存
由于使用额外的内存空间避免了数据交换的耗时,是一种典型的以空间换时间的算法
时间复杂度:O(NlogN)
稳定
堆:把数据当做当做完全二叉树看待,然后把树调整成大顶堆,然后把堆顶数据交换到末尾,然后数量--,然后重新调整回大顶堆,重复操作,直到数量为1时结束,既可以循环实现也可以递归实现(参考heap.c)
时间复杂度:O(NlogN)
不稳定
计数:
找出数据中的最大值和最小值,并创建哈希表,把 数据-最小值 作为数组的下标访问哈希表并标记数量,标记完后,遍历哈希表,当表中的值大于0,把 下标+最小值 还原数据依次放回数组中,是一种典型的以空间换时间的算法
该排序算法理论上速度非常快,它不是基于比较的算法,在一定范围内整数排序时快于任意的一种比较排序算法,但是有很大的局限性:适合排序整形数据,而且数据的范围差别不宜过大,否则会非常浪费内存反而慢于比较的排序,如果数据越平均、重复数越多,性价比越高
时间复杂度:Ο(N+k)(其中k是整数的范围)
稳定的
桶:
根据数据的值存储到不同的桶中,然后再调用其它的排序算法,度桶中的数据进行排序,然后再从桶中依次拷贝回数组中,从而降低排序的规模以此提高排序的速度,是一种典型的以空间换时间的算法
缺点:如何分桶、桶范围多大,这些都需要对数据有一定的了解
时间复杂度:Ο(N+k)
桶排序的稳定性取决于桶内排序使用的算法
基数:
是桶排序的具体实现,首先创建10个队列(链式队列),然后逆序计算出数据的个、十、百...位数,然后入到对应的队列中,结束后依次从队列中出队回数组中,数据下一位继续入队,依次循环,最大值的位数就是循环次数
缺点:只适合排序正整数数据,又要准备队列
时间复杂度:Ο(N+k)
稳定的
几种常用排序算法的实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#include "list_queue.h"
#define LEN 15
#define TYPE int
#define swap(a,b) {typeof(a) t=(a);(a)=(b);(b)=t;}
void show_arr(TYPE* arr,size_t len)
{
for(int i=0; i<len; printf("%02d ",arr[i++]));
printf("\n");
}
typedef void (*SortFP)(TYPE*,size_t);
// 冒泡排序
void bubble_sort(TYPE* arr,size_t len)
{
printf("%s:\n",__func__);
// 标志位用于判断排序是否完成,可以提前继续排序
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);
}
// 选择排序
void select_sort(TYPE* arr,size_t len)
{
printf("%s:\n",__func__);
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(i!=min) swap(arr[i],arr[min]);
}
show_arr(arr,len);
}
// 插入排序
void insert_sort(TYPE* arr,size_t len)
{
printf("%s:\n",__func__);
// i是待插入的数据下标
for(int i=1,j=0; i<len; i++)
{
int val = arr[i];
// 在有序部分找合适的位置插入
for(j=i-1; j>=0 && arr[j]>val; j--)
{
arr[j+1] = arr[j];
}
if(j+1 != i) arr[j+1] = val;
}
show_arr(arr,len);
}
// 希尔排序
void shell_sort(TYPE* arr,size_t len)
{
printf("%s:\n",__func__);
for(int k=len/2; k>0; k/=2)
{
// i是待插入的数据下标
for(int i=k,j=0; i<len; i++)
{
int val = arr[i];
// 在有序部分找合适的位置插入
for(j=i-k; j>=0 && arr[j]>val; j-=k)
{
arr[j+k] = arr[j];
}
if(j+k != i) arr[j+k] = val;
}
}
show_arr(arr,len);
}
void _quick_sort(TYPE* arr,int left,int right)
{
if(left >= right) return;
// 计算标杆下标
int pi = (left+right)/2;
// 备份标杆的值
TYPE pv = arr[pi];
// 备份左右标杆下标
int l = left, r = right;
while(l < r)
{
// 在pi左边找比pv大的数据
while(l<pi && arr[l]<=pv) l++;
if(l < pi)
{
// 找到了比pv大数
arr[pi] = arr[l];
// 更新pi
pi = l;
}
// 从pi右边找比pv小的数
while(r>pi && arr[r]>=pv) r--;
if(r > pi)
{
// 找到比pv小的数
arr[pi] = arr[r];
// 更新pi
pi = r;
}
}
// pi左边小于pv 右边大于pv
// 还原pv
arr[pi] = pv;
if(pi - left > 1) _quick_sort(arr,left,pi-1);
if(right - pi >1) _quick_sort(arr,pi+1,right);
}
// 快速排序
void quick_sort(TYPE* arr,size_t len)
{
_quick_sort(arr,0,len-1);
printf("%s:\n",__func__);
show_arr(arr,len);
}
// 拆分合并 从l到r进行归并
void _merge_sort(TYPE* arr,TYPE* 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);
// 左右部分各自有序
// 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)
{
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,sizeof(TYPE)*(r-l+1));
}
// 归并排序
void merge_sort(TYPE* arr,size_t len)
{
TYPE* tmp = malloc(sizeof(TYPE)*len);
_merge_sort(arr,tmp,0,len-1);
free(tmp);
printf("%s:\n",__func__);
show_arr(arr,len);
}
// 计数排序
void count_sort(TYPE* arr,size_t len)
{
TYPE 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* tmp = calloc(4,max-min+1);
// 标记哈希表
for(int i=0; i<len; i++)
{
tmp[arr[i]-min]++;
}
// 还原数据到arr中
for(int i=0,j=0; i<=max-min; i++)
{
while(tmp[i]--)
{
arr[j++] = i+min;
}
}
free(tmp);
printf("%s:\n",__func__);
show_arr(arr,len);
}
// cnt桶数 ragne桶中数据范围
void _bucket_sort(TYPE* arr,size_t len,int cnt,TYPE range)
{
// 申请桶内存
// bucket指向每个桶的开头
// bucket_end 指向每个桶的末尾
TYPE* bucket[cnt], *bucket_end[cnt];
for(int i=0; i<cnt; i++)
{
// 数据可能全部在一个桶中
bucket[i] = malloc(len*sizeof(TYPE));
// 末尾指针指向开头
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]++;
}
}
}
for(int i=0; i<cnt; i++)
{
// 计算每个桶中元素数量
int size = bucket_end[i] - bucket[i];
// 使用其他排序算法对每个桶进行单独排序
if(1 < size) select_sort(bucket[i],size);
// 把桶按照先后顺序,重新放入数组中
memcpy(arr,bucket[i],size*sizeof(TYPE));
arr += size;
free(bucket[i]);
}
}
// 桶排序
void bucket_sort(TYPE* arr,size_t len)
{
_bucket_sort(arr,len,4,25);
printf("%s:\n",__func__);
show_arr(arr,len);
}
// 基数排序
void radix_sort(TYPE* arr,size_t len)
{
// 创建10个队列
ListQueue* queue[10] = {};
for(int i=0; i<10; i++)
{
queue[i] = create_list_queue();
}
// 计算最大值的位数
TYPE max = arr[0];
for(int i=1; i<len; i++)
{
if(arr[i] > max) max = arr[i];
}
int cnt_max = 0;
while(max)
{
cnt_max++;
max /= 10;
}
// i是1表示个位 2表示十位...
for(int i=1; i<=cnt_max; i++)
{
int mod = pow(10,i);
int div = mod/10;
// 把所有数据入队
for(int j=0; j<len; j++)
{
// 逆序获取每个数的每一位数
int index = arr[j]%mod/div;
// 入到对应下标的队列中
push_list_queue(queue[index],arr[j]);
}
int k = 0;
// 依次把队列数据出队回arr
for(int j=0; j<10; j++)
{
while(!empty_list_queue(queue[j]))
{
arr[k++] = front_list_queue(queue[j]);
pop_list_queue(queue[j]);
}
}
}
for(int i=0; i<10; i++)
{
destory_list_queue(queue[i]);
}
printf("%s:\n",__func__);
show_arr(arr,len);
}
int main(int argc,const char* argv[])
{
TYPE arr[LEN] = {};
// sort 函数指针数组
SortFP sort[] = {bubble_sort,select_sort,insert_sort,shell_sort,quick_sort,merge_sort,count_sort,bucket_sort,radix_sort};
for(int i=0; i<sizeof(sort)/sizeof(sort[0]); i++)
{
for(int j=0; j<LEN; j++)
{
arr[j] = rand()%100;
}
printf("-------------------------------\n");
printf("排序前:\n");
show_arr(arr,LEN);
sort[i](arr,LEN);
}
}
#include "list_queue.h"
Node* create_node(TYPE data)
{
Node* node = malloc(sizeof(Node));
node->data = data;
node->next = NULL;
return node;
}
// 创建
ListQueue* create_list_queue(void)
{
ListQueue* queue = malloc(sizeof(ListQueue));
queue->front = NULL;
queue->tail = NULL;
queue->cnt = 0;
return queue;
}
// 队空
bool empty_list_queue(ListQueue* queue)
{
return 0 == queue->cnt;
}
// 入队
void push_list_queue(ListQueue* queue,TYPE val)
{
Node* node = create_node(val);
if(empty_list_queue(queue))
{
queue->front = node;
queue->tail = node;
}
else
{
queue->tail->next = node;
queue->tail = node;
}
queue->cnt++;
}
// 出队
bool pop_list_queue(ListQueue* queue)
{
if(empty_list_queue(queue)) return false;
Node* temp = queue->front;
queue->front = temp->next;
queue->cnt--;
if(0 == queue->cnt) queue->tail = NULL;
free(temp);
return true;
}
// 队头
TYPE front_list_queue(ListQueue* queue)
{
return queue->front->data;
}
// 队尾
TYPE back_list_queue(ListQueue* queue)
{
return queue->tail->data;
}
// 数量
size_t size_list_queue(ListQueue* queue)
{
return queue->cnt;
}
// 销毁
void destory_list_queue(ListQueue* queue)
{
while(pop_list_queue(queue));
free(queue);
}