基础算法之排序散列递归

基础算法学习笔记(一)

一. 选择排序

1.选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
2. 下面是过程演示:蓝色为有序,[]内为无序,j,k指示元素下标。
在这里插入图片描述
3. 实现

/*选择排序*/
void selectSort(int *a,int n){
	for(int i=0;i<n;i++){ //n趟操作
		int k=i;
		for(int j=i+1;j<n;j++){
			if(a[j]<a[k]){
				k=j;//选出[i,n]中最小的元素,下标为k
			}		
		}
		int temp=a[i];
		a[i]=a[k];
		a[k]=temp;//交换a[k]和a[i]
	
	}
	output(a,n);//输出结果
}

二.插入排序

1.基本思想
插入排序(英语:Insertion Sort)是一种简单直观的排序算法。类似于打扑克摸牌,考虑新摸到的牌该插入的位置。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

2.过程演示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.实现:

/*插入排序*/
void insertSort(int *a,int n){
	for(int i=1;i<n;i++){
		int temp=a[i];//temp暂时存放a[i]
		for(j=i-1;j>=0 && temp<a[j];j--)
			a[j]=a[j-1];//a[j-1]后移一位到a[j]
		a[j]=temp;//插入位置为j	
	}
	output(a,n);	
}

三. sort函数使用

1.介绍:
sort是C ++标准库中用于进行比较排序的通用函数。该函数起源于标准模板库(STL)。它根据具体情形使用不同的排序方法,效率较高。一般来说不推荐使用C语言中qsort函数,因为qsort函数用起来比较繁琐,涉及到很多指针的操作。而且sort在实现中规避了经典快速排序中可能出现的会导致实际复杂度退化到O(n²)的情况。

2.如何使用sort排序
sort函数使用必须加上头文件“#include”和“using namespce std;”,使用方法如下:
sort(首元素地址(必填),尾元素地址,比较函数(非必填));若不写比较函数,则默认对前面给出的区间进行递增排序。

3.实现:

/*定义结构体*/
struct Student {
   char id[10];//学号
   int score;//分数
}stu[3];

/*定义基本数据类型比较函数*/
bool cmp1(const int & a,const int &b){
   return a>b;//按照降序排序
}
/*定义结构体类型比较函数*/
bool cmp(Student a, Student b){
   if(a.score!=b.score)return a.score>b.score;//分数不同则按照分数降序排序
   else return strcmp(a.id,b.id)<0;//分数相同则按照学号升序排序
}

int p[6]={5,2,4,6,3,9};
int q[6]={5,2,4,6,3,9};
sort(p,p+6,cmp1);//调用基本数据类型排序函数
sort(q,q+6);//默认排序

struct Student stu[3]={"12345",98,"12346",77,"12347",98};
sort(stu,stu+3,cmp);//调用结构体类型排序函数

在这里插入图片描述

这里应该注意
比较函数写法1:

    bool cmp(int a,int b){
    return a>b;
    }

和比较函数写法2:

    bool cmp1(const int & a,const int &b){
    	return a>b;//按照降序排序
    }

作为函数参数:int这种写法是值传递,const int&则是引用传递
“值传递”——由于函数将自动产生临时变量用于复制该参数,效率较低。
“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。效率较高。
“引用传递”有可能改变参数,const修饰可以解决这个问题。

扫描二维码关注公众号,回复: 4319268 查看本文章

四.散列(Hash)

1.Hash基本思想
Hash是把关键字Key经过Hash函数映射为整数,这个整数作为Hash表的某个数组下标,尽量唯一的代表这个关键字。
Hash表是一个根据关键字Key而直接进行访问的数据结构,结合了数组和链表两者的优点,即能够在常数级别的时间复杂度内进行按址查找,同时也很容易插入和删除数据。Hash表是对随机存储的优化,先通过Hash函数对所有数据分类,按照类别查找,极大加快查找速度。若Hash函数不存在冲突,能直接找到待查找元素;若Hash函数存在冲突,且通过拉链法解决冲突,能大幅度缩小查找范围。

2.Hash函数
Hash函数建立了关键字Key到Hash表数组下标的映射关系,即:
Address=H(Key)

一个好的Hash函数应该满足2个条件:
(1) Hash函数尽量要简单,保证计算速度;
(2) 尽可能的保证不同的关键字映射为不同的索引地址,也就是避免冲突;

常见的Hash函数有:
(1) 直接定址法:取关键字或者是关键字的线性变换作为Hash表的数组下标;
(2)平方取中法(很少用):取Key的平方的中间若干位作为Hash表的数组下标;
(3)除留余数法:取关键字除以一个数p得到的余数作为Hash表的数组下(Key=Key %p,可以将很大的数转换为不超过p的整数。当p是一个素数时, H(Key)能够覆盖[0,p)内的每一个整数。一般p取不超过Hash表数组长度的最大素数。

冲突是不可避免的。因此,两个相同关键字的Hash值必定相同,但是Hash值相同的两个关键字却未必相同。

冲突的解决方法:
(1) 开放定址法:当一个关键字和另一个关键字发生冲突时,使用某种探测策略在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。根据探测策略的不同,开放定址法又可分为:线性探查法和平方探查法。
(2) 拉链法:和开放定址法不同的是,拉链法并不会对冲突的关键字重新计算Hash值,而是将所有Hash值相同的关键字连接成一条单链表。

3.字符串Hash初步
将字符串映射为整数,可以用于字符串的快速匹配。初步的字符串Hash只讨论将字符串转化为唯一的整数,且整数能够在可表示的范围内。实际上,当字符串较长时,产生的整数会非常大,即使是无符号长整型(unsigned long long)也不能保存。关于对长字符串的Hash进阶在《算法笔记》的12.1节有进一步介绍。

字符串Hash关键是选择合适的进制,一般大于或等于待处理字符串中不同字符的数目,尽量也不要太大,因为进制太大必然会增加Hash表的空间开销。分以下几种情况:
(1) 对于只有大写字母或小写字母的字符串而言,不妨把A-Z(或a到z)映射为0-25,这样26个字母对应到26进制,然后将得到的26进制数转换为10进制数,实现将字符串转换为唯一整数的需求。
(2) 对于既含有大写字母,又含有小写字母,同时又含有数字的字符串而言,可以A-Z作为0-25,a-z作为26-51,0-9作为52-61,此时进制数为62.
4.例题:字符统计
题目描述:
试编写程序,找出一段给定文字中出现最频繁的那个字母。
输入格式:
在一行中给出一个长度不超过1000的字符串。字符串由ASCII表中任意可见字符及空格组成,至少包含一个英文字母,回车键结束。
输出格式:
在一行中输出出现频率最高的那个英文字母和出现次数,其间以空格分开。如果有并列,则输出字母序最小的那个字母。统计时不区分大小写,输出小写字母。
输入样例:
This is a simple TEST. THERE are numbers and other symbols 1&2&3…
输出样例:
e 7

思路:
由于只需要针对英文字母(’A’-‘Z’和’a’-‘z’)输出其中频率最高的次数,所以根本不需要考虑除了英文字母之外的其他字符。并且不区分大小写字母,可以开一个长度为26的Hash表来记录每个字母出现的次数,比如Hashtable[0]=5表示字符串中’a’和’A’共出现了5次。先遍历给定的字符串str,判断各个字符是不是字母以及是大写字母还是小写字母,完成对字符串中字母的统计,最后遍历Hash表获得最大元素即可。

#include <stdio.h>
#include <string.h>
int main(){
	char str[1010];   //字符串
	gets(str);        //读入字符串
	int hashtable[26]={0},   //Hash表,保存字符串中各个字母的个数
		len=strlen(str);
	for(int i=0;i<len;i++)
	{
		if(str[i]>='a'&&str[i]<='z')    //若str[i]是小写字母,出现次数加1
			hashtable[str[i]-'a']++;
		if(str[i]>='A'&&str[i]<='A')    //若str[i]是大写字母,对应的小写字母出现次数加1
			hashtable[str[i]-'A']++;
	}
	int Max=0;      //记录Hash表中最大元素下标
	for(int i=1;i<len;i++)
		if(hashtable[i]>hashtable[Max])
			Max=i;
	printf("%c %d\n",'a'+Max,hashtable[Max]);   //输出字符和出现次数
	return 0;

五.递归

1.分治

分治法可以通俗的解释为:把一片领土分解,分解为若干块小部分,然后一块块地占领征服,被分解的可以是不同的政治派别或是其他什么,然后让他们彼此异化。
分治法的精髓:
分–将问题分解为规模更小的子问题;
治–将这些规模更小的子问题逐个击破;
合–将已解决的子问题合并,最终得出“母”问题的解;
这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
2.递归

程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
递归的缺点:
递归算法解题相对常用的算法如普通循环等,运行效率较低。因此,应该尽量避免使用递归,除非没有更好的算法或者某种特定情况,递归更为适合的时候。在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等。

经典例题:斐波那契数列
斐波那契数列的排列是:0,1,1,2,3,5,8,13,21,34,55,89,144……依次类推下去,你会发现,它后一个数等于前面两个数的和。在这个数列中的数字,就被称为斐波那契数。
递归思想:一个数等于前两个数的和。(这并不是废话,这是执行思路)
首先分析数列的递归表达式:
在这里插入图片描述
在这里插入图片描述

/**
 * 斐波那契数列的递归写法
 *      核心:一个小的解决终点,然后大的问题可以循环在小问题上解决
 * @param n
 * @return
 */
long F(int n){
    if (n<=1) return n;
    return F(n-1)+F(n-2);
}
 
/**
 * 斐波那契数列的递推写法
 * @param n
 * @return
 */
long F1(int n){
    if (n<=1) return n;
    long fn = 0;
    long fn_1 = 1;
    long fn_2 = 0;
    for (int i = 2; i <= n; i++) {
        fn = fn_1 + fn_2;
        fn_2 = fn_1;
        fn_1 = fn;
    }
    return fn;
}

经典例题:N皇后问题:
在N×N格的国际象棋上摆放N个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

#include<stdio.h>
#define Debug
int count=0;
void Queen(int n,int l);
bool CanPut(int i,int j,int (*a)[8],int n);
void Print(int (*a)[8],int n);
 
int main()
{
	printf("请输入棋盘的规模\n");
	int n;
	scanf("%d",&n);
 
	Queen(n,0);
	if(count)
		printf("解的总数是:%d\n",count);
	else
		printf("无解\n");
 
	return 1;
}
 
int a[8][8]={0};
 
void Queen(int n,int l)
{
	if(l==n-1)//最后一行,递归出口
	{
		for(int j=0;j<n;j++)
		{
			if(CanPut(l,j,a,n))//是否能放置皇后
			{
				count++;
				a[l][j]=1;//标记位置
				Print(a,n);
				a[l][j]=0;//取消这个位置
			}
		}
		printf("\n");
	}
	else//不是最后一行
	{--将问题分解为规模更小的子问题;
 
		for(int j=0;j<n;j++)
		{
			a[l][j]=1;//假设这个位置可以放
			if(CanPut(l,j,a,n))//如果可以,递归进入下一行
				Queen(n,l+1);
			a[l][j]=0;//回溯回来后或者不可以放置皇后归0
		}
	}
	return;//如果是if,则递归出口,如果是else,则回溯到上一行
}
 
void Print(int (*a)[8],int n)
{//本算法打印出N皇后的解
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			printf("%d ",a[i][j]);
		}
		printf("\n");
	}
}
 
bool CanPut(int i,int j,int (*a)[8],int n)
{//本算法是判断该位置能否放皇后
	int s=0;
	for(int k=i-1;k>=0;k--)
	{
		s++;
		if(a[k][j]==1 || j+s<n&&a[k][j+s]==1 || j-s>=0&&a[k][j-s]==1)
			return false;
	}
	return true;
}
 

猜你喜欢

转载自blog.csdn.net/weixin_43255133/article/details/83020050