数据结构与算法——查找算法与排序算法

算法:

数据结构中的算法,指的是数据结构所具备的功能
解决特定问题的方法,他是前辈们的一些优秀的经验总结

有穷性:算法的有穷性是指算法必须能在执行有限个步骤之后终止;
确切性:算法的每一步骤必须有确切的定义
输入项:一个算法有0个或多个输入
输出项:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的
可行性:算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个操作步骤都可以在有限时间内完成(也称为有效性)

如何评价一个算法:
    时间复杂度:由于计算机的性能不同,无法准确的衡量出算法执行所需要的时间
    因此我们用算法的执行次数来代表算法的时间复杂度
    一般使用O(公式) 一般忽略常数

常见的时间复杂度:
    //O(1)
    printf("%d",t);

    //O(logn)
    for(int i=n;i>=0;i/=2)
    {
        printf("%d",i);
    }

    //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<n;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)
稳定

//	冒泡排序
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);
}

选择:

假定最开始的位置是最小值并记录下标min,然后与后面的数据比较,如果有比min下标的数据还要小,则更新min,最后判断如果min的值发生了改变,则交换min位置的数据域最开始位置的数据
虽然选择排序的时间复杂度高,但是数据交换次数少,因此实际运行速度并不慢
是冒泡排序的变种,但是没有对数据有序性敏感,数据混乱情况下比冒泡快
时间复杂度:O(n^2)
不稳定(10 10 1)
注意:算法的时间复杂度并不能代表算法的实际时间,有时候时间复杂度高的反而速度更快

//	选择排序
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);
}

插入:

把数据看作两个部分,一部分是有序的,把剩余的数据逐个插入进去
    时间复杂度:O(n^2)
    稳定
//	插入排序
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);
}

希尔:

是插入排序的增强版,由于插入排序数据移动的速度比较慢,所以在此基础上增加了增量的概念,从而提高排序的速度
    时间复杂度:O(n^(1.3~2))
    不稳定
//	希尔排序
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);
}

快速:

找到一个标杆p,备份标杆p的值val,一面从左找比val大的数据,找到后赋值给p位置,更新标杆p的位置到做标杆,然后继续从右边找比val小的数,找到后也赋值给p,同样更新p到右标杆,反复执行知道左右标杆相遇停止,最后把val赋值回p的位置,最终会形成p左边的树都比它小,右边的数都比他大;然后再按找同样的方式对左右两边进行快排,最后全部有序
快速排序的综合性能最高,因此叫做快速排序,笔试面试考最多
时间复杂度:O(nlogn)
不稳定

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);
}    

归并:

先把一组待排序的数据拆分,存放到临时空间中,然后两两比较合并,全部合并完成后再从临时空间中拷贝给原内存
    由于使用额外的内存空间避免了数据交换的耗时,是一种典型的以空间换时间的算法
    时间复杂度:O(nlogn)
    稳定
//	拆分合并 从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);
	
}

堆:

把数据当做完全二叉树看待,然后把数调整成大顶堆,然后把堆顶的数据交换到末尾,然后数量–,然后重新调整回大顶堆,重复操作,直到数量为1时结束,既可以循环实现也可以递归实现(参考heap.c)
时间复杂度:O(nlogn)
不稳定

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define TYPE int
#define SWAP(a,b) {typeof(a) t=(a);(a)=(b);(b)=t;}

//大顶堆
typedef struct Heap
{
	TYPE* arr;
	size_t cal;
	size_t cnt;
}Heap;

//创建
Heap* create_heap(size_t cal)
{
	Heap* heap =malloc(sizeof(Heap));
	heap->arr=malloc(sizeof(TYPE)*cal);
	heap->cal=cal;
	heap->cnt=0;
	return heap;
}

//满堆
bool full_heap(Heap* heap)
{
	return heap->cnt >=heap->cal;	
}

//空堆
bool empty_heap(Heap* heap)
{
	return 0 == heap->cnt;	
}

//添加
bool add_heap(Heap* heap, TYPE data)
{
	if(full_heap(heap))return false;
	heap->arr[heap->cnt++]=data;
	
	//添加位置进行调整,重新形成堆
	int i=heap->cnt;//编号
	while(i>1)
	{
		if(data > heap->arr[i/2-1])
		{
			SWAP(heap->arr[i-1],heap->arr[i/2-1]);
			i=i/2;
		}
		else
			break;
	}
	return true;
}

//删除 只删除堆顶
bool del_heap(Heap* heap)
{
	if(empty_heap(heap)) return false;
//	printf("---%d---\n",heap->arr[heap->cnt-1]);
	//交换堆顶与末尾
	SWAP(heap->arr[0],heap->arr[heap->cnt-1]);
	heap->cnt--;
	//从上往下调整
	int i=1; //编号
	while(i <= heap->cnt)
	{
		//有右子树
		if(i*2+1<=heap->cnt)
		{
			//右子树最大 交换右和根
			if(heap->arr[i*2] >heap->arr[i*2-1] && heap->arr[i*2] > heap->arr[i-1])
			{
				SWAP(heap->arr[i*2],heap->arr[i-1]);
				i=i*2+1;
			}
			//左子树最大,交换左根
			else if(heap->arr[i*2-1] > heap->arr[i-1])
			{
				SWAP(heap->arr[i*2-1],heap->arr[i-1]);
				i=i*2;
			}
			//根最大
			else
				break;
		}
		//有左子树,没有右子树
		else if(i*2 <= heap->cnt)
		{
			if(heap->arr[i*2-1] > heap->arr[i-1])
			{
				SWAP(heap->arr[i*2-1],heap->arr[i-1]);
				i=i*2;
			}
			else
				break;
		}
		//没有左右子树
		else
			break;
	}
	return true;
}

//遍历
void show_heap(Heap* heap)
{
	for(int i=0;i<heap->cnt;i++)
	{
		printf("%d ",heap->arr[i]);	
	}
	printf("\n");
}

//堆顶
TYPE top_heap(Heap* heap)
{
	return heap->arr[0];
}

//堆排序 顺序实现
void sort_heap(int* arr,int len)
{
	//把数组调成堆结构
	for(int i=1;i<=len;i++)
	{
		int j=i;
		while(j>1)
		{
			if(arr[j-1]>arr[j/2-1])	
			{
				SWAP(arr[j-1],arr[j/2-1]);
				j=j/2;
			}
			else
				break;
		}
	}
	//删除堆顶,直到堆为空
	while(len>1)
	{
		//交换堆顶 末尾
		SWAP(arr[0],arr[len-1]);
		len--;
		
		//从上往下调整
		int i=1;
		while(i<=len)
		{
			if(i*2+1<=len)
			{
				if(arr[i*2]>arr[i*2-1] && arr[i*2]> arr[i-1])
				{
					SWAP(arr[i*2],arr[i-1]);
					i=i*2+1;
				}
				else if(arr[i*2-1]>arr[i-1])
				{
					SWAP(arr[i*2-1],arr[i-1]);
					i=i*2;
				}
				else
					break;
			}
			else if(i*2<=len)
			{
				if(arr[i*2-1]>arr[i-1])
				{
					SWAP(arr[i*2-1],arr[i-1]);
					i=i*2;
				}
				else break;
			}
			else
				break;
		}
	}
}

//从top下标到end下标 从上往下调整成堆结构
void _sort_heap_recursion(int* arr,int top,int end)
{
	if(top>=end)return;
	int max=top+1; //max是左右根中最大值的编号
	int l=max*2;
	int r=max*2+1;
	if(l-1<=end && arr[l-1]>arr[max-1])
	{
		//有左子树且左子树大于max,更新max
		max=l;
	}
	if(r-1<=end && arr[r-1]>arr[max-1])
	{
		//有右子树,且右子树大于max的值,更新max
		max=r;
	}
	if(max-1 != top)
	{
		//max是左右根中最大的,交换根与max
		SWAP(arr[top],arr[max-1]);
		_sort_heap_recursion(arr,max-1,end);
	}
}

//堆排序的递归实现
void sort_heap_recursion(int* arr,int len)
{
	//把数组调成堆结构
	for(int i=2;i<=len;i++)
	{
		int j=i;
		while(j>1)
		{
			if(arr[j-1]>arr[j/2-1])	
			{
				SWAP(arr[j-1],arr[j/2-1]);
				j=j/2;
			}
			else
				break;
		}
	}
	for(int i=len-1;i>0;i--)
	{
		SWAP(arr[0],arr[i]);
		_sort_heap_recursion(arr,0,i-1);
	}
		
}

void show_arr(int* arr,int len)
{
	for(int i=0;i<len;i++)
	{
		printf("%d ",arr[i]);	
	}
	printf("\n");
}

int main(int argc,const char* argv[])
{
	int arr[10]={};
	for(int i=0;i<10;i++)
	{
		arr[i]=rand()%100;
	}
	sort_heap_recursion(arr,10);
	show_arr(arr,10);
	
}

计数:找出数据中的最大值和最小值,并创建哈希表,把数据的最小值作为数组的下标访问并标记数量,标记完后,遍历哈希表,当表中的值大于0,把下标+最小值还原数据依次放回数组中

    该排序算法理论上速度非常快,它不是基于比较的算法,但是有很大的局限性:适合排序整型数据,而且数据的范围差别不宜过大,否则会非常浪费内存反而慢于比较的排序,如果数据越平均、重复数越多,性价比越高
    时间复杂度:O(n+k)(其中k是整数的范围)
    稳定
//	计数排序
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);
}

桶:

根据数据的值存储到不同的桶中,然后再调用其他的排序算法,对同种5的数据进行排序,然后再从桶中依次拷贝会数组中,从而降低排序的规模,以此提高排序的速度,是一种典型的以空间换时间的算法
    缺点:如何分桶、桶范围多大,这些都需要对数据有一定的了解
    时间复杂度:O(n+k)
    桶排序的稳定性取决于桶内排序使用的算法
 //	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);
}

基数:

桶排序的具体实现,首先创建10个队列(链式队列),然后逆序计算出数据的个、十、百...位数,然后入到对应的队列中,结束后依次从队列中出队回到数组中,数据下一位继续入队,依次循环,最大值的位数就是循环次数
    缺点:只适合排序正整数数据,又要准备队列
    时间复杂度:O(n+k)
    稳定
//	基数排序
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);
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值