精华内容
下载资源
问答
  • 下列数据不具有排序功能的有
    万次阅读 多人点赞
    2020-10-24 10:30:59

    题目:实现线性表的基本操作:分别定义一个包含学生信息(学号,姓名,成绩)的顺序表和链表,使其具有如下功能:
    (1) 根据指定学生个数,逐个输入学生信息;
    (2) 逐个显示学生表中所有学生的相关信息;
    (3) 根据姓名进行查找,返回此学生的学号和成绩;
    (4) 根据指定的位置可返回相应的学生信息(学号,姓名,成绩);
    (5) 给定一个学生信息,插入到表中指定的位置;
    (6) 删除指定位置的学生记录;
    (7) 统计表中学生个数。

    本文提供顺序表和链表两种算法解决方案

    解决方案

    1. 首先要对链表和顺序表存储结构进行定义,链表定义结点数据域和指针域,顺序表定义存储空间基地址和长度。然后对链表和顺序表进行初始化,初始化就是构造一个空的表。
    2. 输入学生信息和在指定位置位置插入学生信息都是用ListInsert函数进行插入,算法一样。
    3. 显示学生信息是将每一个学生信息在表中进行取值然后进行输出,顺序表取值可以直接通过数组数组下标定位第i个元素值,链表取值不能随机访问,需要从链表的首元结点出发,顺着链域next逐个结点向下访问。
    4. 在链表查询时,先输入学生姓名,利用search函数进行查找。先初始化,用指针p指向首元结点,用strcmp函数对p所指结点数据域的姓名值与输入的姓名进行对比,如果相同返回结点地址p,然后将结点p的数据域进行输出。
    5. 显示指定位置学生信息和显示学生信息算法相同,显示学生信息是显示所有的信息,显示指定位置的学生信息是显示单个学生信息,都是顺序表的取值操作。
    6. 在链表插入时,先录入要插入的位置,然后用input函数将数据存在c结点中,再用ListInsert函数将结点插入链表,先查找插入位置的前一个结点,将指针p指向该结点。生成一个新节点s,将结点s的数据域置为输入的c结点,将 结点s的指针域指向下一个结点,将结点p的指针域指向结点s。
    7. 删除指定位置的学生记录是线性表的删除操作。顺序表删除主要是被删除元素之后元素前移,然后将表长减一。链表删除时先找到要删除元素前一个元素,指针p指向该结点,临时保存被删除结点的地址以备释放,将要删除结点的前驱结点指针域指向要删除结点的直接后继结点,释放删除结点空间。

    运行结果
    1.构造链表
    在这里插入图片描述
    2. 录入学生信息
    在这里插入图片描述
    3.显示学生信息
    在这里插入图片描述
    4.根据姓名查找
    在这里插入图片描述
    5.显示指定位置的学生信息
    在这里插入图片描述
    6.在指定位置插入学生信息
    在这里插入图片描述
    7.删除指定位置学生信息
    在这里插入图片描述
    顺序表实现

    #include<stdio.h>
    #include<malloc.h>
    #include<stdlib.h>
    #include<string.h>
    #define MAXSIZE 100
    
    typedef int Status; 
    
    //学生信息定义 
    typedef struct {
        char no[8];   //8位学号
        char name[20]; //姓名
        double price;     //成绩
    }student;
    typedef student ElemType;
    
    //顺序表存储结构
    typedef struct{
    	ElemType *elem;
    	int length;
    }SqList;
    
    //顺序表初始化 
    Status InitList(SqList &L){
    	L.elem=new ElemType[MAXSIZE];
    	if(!L.elem) exit(0);
    	L.length=0;
    	return 1;
    }
    //返回学生信息 
    ElemType GetElem(SqList L,int i){
    	return L.elem[i];
    }
    // 根据名字查找
    int Search(SqList &L,char str[])  
    {
    	for(int i=1;i<=L.length;i++)
    	{
    		if(strcmp(L.elem[i].name,str)==0)
    			return i;
    	}
    	return 0;
    }
    // 在 i位置插入某个学生的信息 
    Status ListInsert(SqList &L,int i,ElemType e)
    {
    	if((i<1)||(i>L.length+1))	return 0;
    	if(L.length==MAXSIZE)	return 0;
    	for(int j=L.length;j>=i;j--)
    	{
    		L.elem[j+1]=L.elem[j];
    	}
    	L.elem[i]=e;
    	++L.length;
    	return 1;
    }
     // 在顺序表中删除 i位置的学生信息
    Status ListDelete(SqList &L,int i)  
    {
    	if((i<1)||(i>L.length))	return 0;
    	for(int j=i;j<=L.length;j++)
    	{
    		L.elem[j]=L.elem[j+1];
    	}
    	--L.length;
    	return 1;
    }
     
    void Input(ElemType *e)
    {
    	printf("姓名:");	scanf("%s",e->name);
    	printf("学号:");	scanf("%s",e->no);
    	printf("成绩:");	scanf("%lf",&e->price);
    	printf("输入完成\n\n");
    }
     
    void Output(ElemType *e)
    {
    	printf("姓名:%-20s\n学号:%-10s\n成绩:%-10.2lf\n\n",e->name,e->no,e->price);
    }
     
    int main(){
    	SqList L;
    	ElemType a,b,c,d;
    	printf("\n1.构造顺序表\n");
    	printf("2.输入学生信息\n");
    	printf("3.显示学生表信息\n");
    	printf("4.根据姓名进行查找\n");
    	printf("5.显示指定的位置学生信息\n");
    	printf("6.在指定位置插入学生信息\n");
    	printf("7.删除指定位置的学生记录\n");
    	printf("8.统计学生人数\n");
    	printf("9.退出\n\n");
    	int n,choose;
    	while(1) {
    		printf("请选择:");
    		scanf("%d",&choose);
    		if(choose==9) break;
    		switch(choose){
    			case 1:if(InitList(L))  
    			           printf("成功建立顺序表\n");
    			        else 
    					   printf("失败");
    					break;
    			case 2:	printf("请输入要录入的学生人数:");
    					scanf("%d",&n);
    					for(int i=1;i<=n;i++)
    					{
    						printf("第%d个学生:\n",i);
    						Input(&L.elem[i]);
    					}
    					L.length=n;
    					break;
    			case 3:for(int i=1;i<=n;i++)
    					{
    						a=GetElem(L,i);
    						Output(&a);
    					}
    					break;
    			case 4:	char str[15];
    					printf("请输入要查找的学生姓名:");
    					scanf("%s",str);
    					if(Search(L,str))
    						Output(&L.elem[Search(L,str)]);
    					else
    						printf("没有此学生信息!\n");
    					break;
    			case 5:printf("请输入要查询的位置:");
    					int id1;
    					scanf("%d",&id1);
    					b=GetElem(L,id1);
    					Output(&b);
    					break;
    			case 6:	printf ("请输入要插入的位置:");
    					int id2;
    					scanf("%d",&id2);
    					printf("请输入学生信息:\n");
    					Input(&c);
    					if(ListInsert(L,id2,c))
    					{
    						n++;
    						puts("插入成功");
    					}
    					else
    					{
    						puts("插入失败");	
    					}
    					break;
    			case 7:printf("请输入要删除的位置:");
    					int id3;
    					scanf("%d",&id3);
    					if(ListDelete(L,id3))
    					{
    						n--;
    						puts("删除成功");
    					}
    					else
    					{
    						puts("删除失败");
    					}
    					break;
    			case 8:printf("已录入的学生个数为:%d\n\n",L.length);
    				   break;	
    		}
    	}
    	return 0;
    }
    

    链表实现

    #include<stdio.h>
    #include<malloc.h>
    #include<stdlib.h>
    #include<string.h>
    
    typedef int Status; 
    
    //学生信息定义 
    typedef struct {
        char no[8];   //8位学号
        char name[20]; //姓名
        double price;     //成绩
    }student;
    
    typedef student ElemType;
    
    //链表存储结构
    typedef struct LNode{
        ElemType   data;       //数据域
        struct LNode  *next;   //指针域
    }LNode,*LinkList;
    
    //链表初始化 
    Status InitList(LinkList &L){
    	L=new LNode;
    	L->next=NULL; 
    	return 1;
    }
    //显示学生表信息 
    Status GetElem(LinkList L,int i,ElemType &e) 
    {
    	LinkList p;
    	p=L->next;
    	int j=1;
    	while(p&&j<i)
    	{
    		p=p->next;
    		++j;
    	}
    	if(!p||j>i)	
    	    return 0;
    	e=p->data;
    	return 1;
    }
    // 根据名字进行查找 
    Status Search(LNode L,char str[],LinkList &p)
    {
    	p=L.next; 
    	while(p)
    	{
    		if(strcmp(p->data.name,str)==0)
    			return 1;
    		p=p->next;
    	}
    	return 0;
    }
    // 在指定位置插入某个学生的信息
    Status ListInsert(LinkList L,int i,ElemType e) 
    {
    	LinkList p,s;
    	p=L;
    	int j=0;
    	while(p&&j<i-1)
    	{
    		p=p->next;
    		++j;
    	}
    	if(!p||j>i-1)	
    	    return 0;
    	s=new LNode;
    	s->data=e;
    	s->next=p->next;
    	p->next=s;
    	return 1;
    }
     // 删除指定位置的学生信息 
    Status ListDelete(LinkList p,int i) 
    {
    	int j=0;
    	while((p->next)&&(j<i-1))
    	{
    		p=p->next;
    		++j;
    	}
    	if(!(p->next)||(j>i-1))	return 0;
    	LinkList q;
    	q=p->next;
    	p->next=q->next; 
    	delete q;
    	return 1;
    }
     
    void Input(ElemType *e)
    {
    	printf("姓名:");	scanf("%s",e->name);
    	printf("学号:");	scanf("%s",e->no);
    	printf("成绩:");	scanf("%lf",&e->price);
    	printf("输入完成\n\n");
    }
     
    void Output(ElemType *e)
    {
    	printf("姓名:%-20s\n学号:%-10s\n成绩:%-10.2lf\n\n",e->name,e->no,e->price);
    }
     
    int main(){
    	LNode L;
    	LinkList p;
    	ElemType a,b,c,d;
    	printf("\n1.构造链表\n");
    	printf("2.输入学生信息\n");
    	printf("3.显示学生表信息\n");
    	printf("4.根据姓名进行查找\n");
    	printf("5.显示指定的位置学生信息\n");
    	printf("6.在指定位置插入学生信息\n");
    	printf("7.删除指定位置的学生记录\n");
    	printf("8.统计学生人数\n");
    	printf("9.退出\n\n");
    	int n,choose;
    	while(1) {
    		printf("请选择:");
    		scanf("%d",&choose);
    		if(choose==9) break;
    		switch(choose){
    			case 1:if(InitList(p))  
    			           printf("成功建立链表\n");
    			        else 
    					   printf("失败");
    					break;
    			case 2:	printf("请输入要录入的学生人数:");
    					scanf("%d",&n);
    					for(int i=1;i<=n;i++)
    					{
    						printf("第%d个学生:\n",i);
    						Input(&a);
    						ListInsert(&L,i,a);
    					}
    					break;
    			case 3:for(int i=1;i<=n;i++)
    					{
    						GetElem(&L,i,b);
    						Output(&b);
    					}
    					break;
    			case 4:	char str[15];
    					printf("请输入要查找的学生姓名:");
    					scanf("%s",str);
    					if(Search(L,str,p))
    						Output(&(p->data));
    					else
    						printf("没有此学生信息!\n");
    					break;
    			case 5:printf("请输入要查询的位置:");
    					int d1;
    					scanf("%d",&d1);
    					GetElem(&L,d1,c);
    					Output(&c);
    					break;
    			case 6:	printf ("请输入要插入的位置:");
    					int d2;
    					scanf("%d",&d2);
    					printf("请输入学生信息:\n");
    					Input(&c);
    					if(ListInsert(&L,d2,c))
    					{
    						n++;
    						puts("插入成功");
    					}
    					else
    					{
    						puts("插入失败");	
    					}
    					break;
    			case 7:printf("请输入要删除的位置:");
    					int d3;
    					scanf("%d",&d3);
    					if(ListDelete(&L,d3))
    					{
    						n--;
    						puts("删除成功");
    					}
    					else
    					{
    						puts("删除失败");
    					}
    					break;
    			case 8:printf("已录入的学生个数为:%d\n\n",n);
    				   break;	
    		}
    	}
    	return 0;
    }
    
    更多相关内容
  • 数据结构:排序(Sort)【详解】

    千次阅读 多人点赞 2021-03-12 12:11:01
    友情链接:数据结构专栏 目录排序【知识框架】排序概述一、排序的相关定义二、排序用到的结构与函数常见的排序算法一、冒泡排序 排序 【知识框架】 排序概述 一、排序的相关定义 排序,就是重新排列表中的元素,...

    排序

    【知识框架】

    在这里插入图片描述

    排序概述

    一、排序的相关定义

    1. 排序,就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。为了查找方便,通常希望计算机中的表是按关键字有序的。排序的确切定义如下:
      输入: n n n个记录 R 1 , R 2 , . . . , R n R_1,R_2,...,R_n R1,R2,...,Rn,对应的关键字为 k 1 , k 2 , . . . , k n k_1,k_2,...,k_n k1,k2,...,kn
      输出:输入序列的一个重排 R 1 ′ , R 2 ′ , . . . , R n ′ {R_1}^{'},{R_2}^{'},...,{R_n}^{'} R1,R2,...,Rn,使得 k 1 ′ ≤ k 2 ′ ≤ . . . ≤ k n ′ {k_1}^{'}≤{k_2}^{'}≤...≤{k_n}^{'} k1k2...kn(其中“ ≤ ≤ ”可以换成其他比较大小的符号)

    2. 排序的稳定性。假设 k i = k j ( 1 ≤ i ≤ n , 1 ≤ j ≤ n , i ! = j ) k_i=k_j(1≤i≤n,1≤j≤n,i!=j) ki=kj(1in,1jn,i!=j),且在排序前的序列中 R i R_i Ri领先于 R j R_j Rj(即 i < j i<j i<j)。如果排序后 R i R_i Ri仍领先于 R j R_j Rj,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中 R j R_j Rj领先于 R i R_i Ri,则称所用的排序方法是不稳定的。
      需要注意的是,算法是否具有稳定性并不能衡量一个算法的优劣,它主要是对算法的性质进行描述。如果待排序表中的关键字不允许重复,则排序结果是唯一的,那么选择排序算法时的稳定与否就无关紧要。

    3. 内排序和外排序。内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。

    通常可以将排序算法分为插入排序、交换排序、选择排序、归并排序和基数排序五大类。每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为 是最好的算法。内部排序算法的性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动的次数决定的。

    二、排序用到的结构与函数

    以下是一个用于排序用的顺序表结构。

    #define MAXSIZE 10	//用于要排序数组个数最大值,可根据需要修改
    typedef struct{
    	int R[MAXSIZE];	//用于存储要排序的数组
    	int length;	//用于记录顺序变的长度
    }SqList;
    

    另外,由于排序最常用到的操作是数组两元素的交换,所以我们将它写成函数。

    /*交换L中数组r的下标为i和j的值*/
    void swap(SqList *L, int i, int j){
    	int temp = L->R[i];
    	L->R[i] = L->R[j];
    	L->R[j] = temp;
    }
    

    常见的排序算法

    一、冒泡排序(交换排序)

    1、算法

    冒泡排序的基本思想是:从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换它们,直到序列比较完。第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置(或将最大的元素交换到待排序列的最后一个位置),关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”(或关键字最大的元素如石头一般下沉至水底)。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素(或最大元素)放到了序列的最终位置…这样最多做 n − 1 n-1 n1趟冒泡就能把所有元素排好序。
    如下图所示,我们对序列 { 49 , 38 , 65 , 97 , 76 , 13 , 27 , 49 } \{49,38,65,97,76,13,27,49\} {49,38,65,97,76,13,27,49}进行冒泡排序:
    在这里插入图片描述
    冒泡排序算法的代码如下:

    void BubbleSort(SqList *L){
    	int i, j;
    	bool flag = true;	//表示本趟冒泡是否发生交换的标志
    	for(i=0; i< L->length-1; i++){	
    		flag = false;	
    		for(j=n-1; j>i; j--){	//一趟冒泡过程
    			if(L->R[j-1] > L->R[j]){	//若为逆序
    				swap(&L, j-1, j);	//交换
    				flag = true;
    			}
    		}
    		if(flag == false){
    			return;	//本趟遍历后没有发生交换,说明表已经有序
    		}
    	}
    }
    

    2、性能分析

    空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O ( 1 ) O(1) O(1)
    时间效率:当初始序列有序时,显然第一趟冒泡后flag依然为false (本趟冒泡没有元素交换),从而直接跳出循环,比较次数为 n − 1 n-1 n1,移动次数为 0 0 0,从而最好情况下的时间复杂度为 O ( n ) O(n) O(n);当初始序列为逆序时,需要进行 n − 1 n- 1 n1趟排序,第 i i i趟排序要进行 n − i n -i ni次关键字的比较,而且每次比较后都必须移动元素 3 3 3次来交换元素位置。这种情况下, 比 较 次 数 = ∑ i = 1 n ( n − i ) = n ( n − 1 ) / 2 比较次数=\displaystyle\sum_{i=1}^{n}(n-i)=n(n-1)/2 =i=1n(ni)=n(n1)/2 移 动 次 数 = ∑ i = 1 n 3 ( n − i ) = 3 n ( n − 1 ) / 2 移动次数=\displaystyle\sum_{i=1}^{n}3(n-i)=3n(n-1)/2 =i=1n3(ni)=3n(n1)/2从而,最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2),其平均时间复杂度也为 O ( n 2 ) O(n^2) O(n2)


    二、简单选择排序

    简单选择排序法(Simple Selection Sort) 就是通过 n − i n-i ni次关键字间的比较,从 n − i + 1 n-i+1 ni+1个记录中选出关键字最小的记录,并和第 i ( 1 < i < n ) i (1<i<n) i(1<i<n)个记录交换之。

    1、算法

    void SelectSort(SqList *L){
    	int i,j,min;
    	for(i=0; i<L->length-1;i++){	//一共进行n-1趟
    		min = i;	//记录最小元素位置
    		for(j=i+i; j<L->length; j++){
    			if(L->R[j] < L->R[min]){	//在R[i...n-1]中选择最小的元素
    				min = j;	//更新最小元素位置
    			}
    		}
    		if(min !=i){
    			swap(L->R[i], L->R[min]);	//swap函数移动元素3次
    		}
    	}
    }
    

    2、性能分析

    空间效率:仅使用常数个辅助单元,故空间效率为 O ( 1 ) O(1) O(1)
    时间效率:从上述代码中不难看出,在简单选择排序过程中,元素移动的操作次数很少,不会超过 3 ( n − 1 ) 3(n-1) 3(n1)次,最好的情况是移动 0 0 0次,此时对应的表已经有序;但元素间比较的次数与序列的初始状态无关,始终是 n ( n − 1 ) / 2 n(n- 1)/2 n(n1)/2次,因此时间复杂度始终是 O ( n 2 ) O(n^2) O(n2)
    稳定性:在第 i i i趟找到最小元素后,和第 i i i个元素交换,可能会导致第 i i i个元素与其含有相同关键字元素的相对位置发生改变。例如,表 L = { 2 , 2 , 1 } L= \{2,2, 1\} L={2,2,1},经过一趟排序后 L = { 1 , 2 , 2 } L= \{1, 2,2\} L={1,2,2},最终排序序列也是 L = { 1 , 2 , 2 } L=\{1,2,2\} L={1,2,2},显然, 2 2 2 2 2 2的相对次序已发生变化。因此,简单选择排序是一种不稳定的排序方法。


    三、直接插入排序

    直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。

    1、算法

    void InsertSort(SqList *L){
    	int i,j;
    	//依次将R[2]~R[n]插入到前面已排序序列,R[1]为默认排好序的序列,R[0]作为哨兵不存放元素
    	for(i=2; i<=L->length; i++){
    		//若R[i]关键码小于其前驱,将A[i]插入有序表
    		if(L->R[i] < L->R[i-1]){
    			L->R[0] = L->R[i];	//复制为哨兵,R[0]不存放元素
    			//从后往前查找待插入位置
    			for(j=i-1; L->R[0]<L->R[j]; --j){
    				L->R[j+1] = L->R[j];	//向后挪位
    			}
    			L->[j+1] = A[0];	//复制到插入位置
    		}
    	}
    }
    

    假定初始序列为 { 49 , 38 , 65 , 97 , 76 , 13 , 27 , 49 } \{49, 38, 65, 97 ,76, 13, 27, 49\} {49,38,65,97,76,13,27,49},初始时 49 49 49可以视为一个已排好序的子序列,按照上述算法进行直接插入排序的过程如下图所示,括号内是已排好序的子序列。
    在这里插入图片描述

    2、性能分析

    空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O ( 1 ) O(1) O(1)
    时间效率:在排序过程中,向有序子表中逐个地插入元素的操作进行了 n − 1 n-1 n1趟,每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。
    在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为 O ( n ) O(n) O(n)
    在最坏情况下,表中元素顺序刚好与排序结果中的元素顺序相反(逆序),总的比较次数达到最大,为 ∑ i = 2 n i \displaystyle\sum_{i=2}^{n}i i=2ni,总的移动次数也达到最大,为 ∑ i = 2 n ( i + 1 ) 。 \displaystyle\sum_{i=2}^{n}(i+1)。 i=2n(i+1)
    平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数均约为 n 2 / 4 n^2/4 n2/4
    因此,直接插入排序算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    稳定性:由于每次插入元素时总是从后向前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序方法。
    适用性:直接插入排序算法适用于顺序存储和链式存储的线性表。为链式存储时,可以从前往后查找指定元素的位置。


    四、折半插入排序

    从直接插入排序算法中,每趟插入的过程中都进行了两项工作:①从前面的有序子表中查找出待插入元素应该被插入的位置;②给插入位置腾出空间,将待插入元素复制到表中的插入位置。注意到在该算法中,总是边比较边移动元素。下面将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素。

    1、算法

    void HalfInsertSort(SqList *L){
    	int i,j,low,high,mid;
    	//依次将R[2]~R[n]插入前面的已排序序列
    	for(i=2; i<=L->length;i++){
    		L->R[0] = L->R[i];	//将R[i]暂存到R[0]
    		low=1; high=i-1;	//设置折半查找的范围
    		//折半查找(默认递增有序)
    		while(low <= high){
    			mid = (low + high) / 2;	//取中间点
    			if(L->R[mid] > L->R[0]){
    				high = mid-1;	//查找左半子表
    			}else{
    				low = mid+1;	//查找右半子表
    			}
    		}
    		for(j = i-1; j>=high+1; --j){
    			L->R[j+1] = L->R[j];	//统一后移元素,
    		}
    		L->R[high+1] = L->R[0];	//插入操作
    	}
    }
    

    2、性能分析

    从上述算法中,不难看出折半插入排序仅减少了比较元素的次数,约为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数 n n n;而元素的移动次数并未改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍为 O ( n 2 ) O(n^2) O(n2),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。折半插入排序是一种稳定的排序方法。


    五、希尔排序

    希尔排序是对直接插入排序进行改进而得来的,又称缩小增量排序。
    希尔排序的基本思想是:先将待排序表分割成若干形如 L [ i , i + d , i + 2 d , . . . , i + k d ] L[i,i+d,i+ 2d,...,i+ kd] L[i,i+d,i+2d,...,i+kd]的“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
    希尔排序的过程如下:先取一个小于 n n n的步长 d 1 d_1 d1,把表中的全部记录分成 d 1 d_1 d1组,所有距离为 d 1 d_1 d1的倍数的记录放在同一组,在各组内进行直接插入排序;然后取第二个步长 d 2 < d 1 d_2<d_1 d2<d1,重复上述过程,直到所取到的 d t = 1 d_t= 1 dt=1,即所有记录已放在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故可以很快得到最终结果。

    1、算法

    /*对顺序表L作希尔排序*/
    void ShellSold(SqList *L){
    	int i,j;
    	int increment = L->length;	
    	do{
    		increment = increment/3 + 1;	//增量序列
    		for(i = increment+1; i <= L->length; i++){
    			if(L-R[i] < L->R[i-increment]){
    				/*需将L->R[i]插入有序增量子表*/
    				L->R[0] = L->R[i];	//暂存R[0]
    				for(j = i-increment; j>0 && L->R[0]<L->R[j]; j-=increment){
    					L->R[j+increment] = L->R[j];	//记录后移,查找插入位置
    				}
    				L->R[j+increment] = L->R[0];	//插入
    			}
    		}
    	}while(increment > 1);
    }
    

    例如,当传入的SqList的的 l e n g t h = 9 length=9 length=9 R [ 10 ] = { 0 , 9 , 1 , 5 , 8 , 3 , 7 , 4 , 6 , 2 } R[10]=\{0,9,1,5,8,3,7,4,6,2\} R[10]={0,9,1,5,8,3,7,4,6,2}。排序的大致过程下图所示。

    第一轮increment=4:
    在这里插入图片描述
    第二轮increment=2:
    在这里插入图片描述
    第三轮increment=1:
    在这里插入图片描述
    最终结果:
    在这里插入图片描述

    2、性能分析

    空间效率:仅使用了常数个辅助单元,因而空间复杂度为 O ( 1 ) O(1) O(1)
    时间效率:由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当 n n n在某个特定范围时,希尔排序的时间复杂度约为 O ( n 3 / 2 ) O(n^{3/2}) O(n3/2)。要好于直接排序的 O ( n 2 ) O(n^2) O(n2)
    稳定性:当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序方法。
    适用性:希尔排序算法仅适用于线性表为顺序存储的情况。


    六、堆排序

    堆排序(Heap Sort)是对简单选择排序进行的一种改进。

    1、堆的定义

    堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大根堆(如下图所示);或者每个结点的值都小于或等于其左右孩子结点的值,称为小根堆。
    在这里插入图片描述

    2、堆排序

    堆排序的思路很简单:首先将存放在 L [ 1... n ] L[1...n] L[1...n]中的 n n n个元素建成初始堆,由于堆本身的特点(以大顶堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。可见堆排序需要解决两个问题:①如何将无序序列构造成初始堆?②输出堆顶元素后,如何将剩余元素调整成新的堆?

    堆排序的关键是构造初始堆 n n n个结点的完全二叉树,最后一个结点是第 ⌊ n / 2 ⌋ ⌊n/2⌋ n/2个结点的孩子。对第 ⌊ n / 2 ⌋ ⌊n/2⌋ n/2个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对各结点( ⌊ n / 2 ⌋ − 1 ⌊n/2⌋ -1 n/21~ 1 1 1)为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点。
    举例自上往下逐步调整为大根堆,如下图所示。
    在这里插入图片描述
    继续已上图为例:
    输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。将 09 09 09和左右孩子的较大者 78 78 78交换,交换后破坏了子树的堆,继续对子树向下筛选,将 09 09 09和左右孩子的较大者 65 65 65交换,交换后得到了新堆,调整过程如下图所示。
    在这里插入图片描述

    3、算法

    下面是建立大根堆的算法:

    void BuildMaxHeap(ElemType A[], int len){
    	for(int i=len/2; i>0; i--){	//从i=[n/2]~1,反复调整堆
    		HeadAdjust(A, i, len);
    	}
    }
    

    函数HeadAdjust将元素k为根的子树进行调整。

    /*函数HeadAdjust将元素k为根的子树进行调整*/
    void HeadAdjust(ElemType A[], int k, int len){
    	A[0] = A[k];	//A[0]暂存子树的根节点
    	for(i=2*k; i<=len; i*=2){	//沿key较大的子结点向下筛选
    		if(i<len && A[i]<A[i+1]){
    			i++;	//取key较大的子节点的下标
    		}
    		if(A[0] >= A[i]){
    			break;	//筛选结束
    		}else{
    			A[k] = A[i];	//将A[i]调整到双亲结点上
    			k = i;	//修改k值,以便继续向下筛选
    		}
    	}
    	A[k] = A[0];	//被筛选结点的值放入最终位置
    }
    

    调整的时间与树高有关,为 O ( h ) O(h) O(h)。在建含 n n n个元素的堆时,关键字的比较总次数不超过 4 n 4n 4n,时间复杂度为 O ( n ) O(n) O(n),这说明可以在线性时间内将一个无序数组建成一个堆。
    下面是堆排序算法:

    void HeapSord(ElemType A[], int len){
    	BuildMaxHeap(A, len);	//初始建堆
    	for(i = len; i>1; i--){	//n-1趟的交换和建堆过程
    		Swap(A, i, 1);	//输出堆顶元素(和堆底元素交换)
    		HeapAdjust(A, 1, i-1);	//调整,把剩余的i-1个元素整理成堆
    	}
    }
    

    同时,堆也支持插入操作。对堆进行插入操作时,先将新结点放在堆的末端,再对这个新结点向上执行调整操作。大根堆的插入操作示例如下图所示。
    在这里插入图片描述

    堆排序适合关键字较多的情况(如n>1000)。例如,在1亿个数中选出前100个最大值?首先使用一个大小为100的数组,读入前100个数,建立小顶堆,而后依次读入余下的数,若小于堆顶则舍弃,否则用该数取代堆顶并重新调整堆,待数据读取完毕,堆中100个数即为所求。

    4、性能分析

    空间效率:仅使用了常数个辅助单元,所以空间复杂度为 O ( 1 ) O(1) O(1)
    时间效率:建堆时间为 O ( n ) O(n) O(n),之后有 n − 1 n-1 n1次向下调整操作,每次调整的时间复杂度为 O ( h ) O(h) O(h),故在最好、最坏和平均情况下,堆排序的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
    稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法。


    七、归并排序

    1、算法

    归并排序与上述基于交换、选择等排序的思想不一样,“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表。假定待排序表含有 n n n个记录,则可将其视为 n n n个有序的子表,每个子表的长度为 1 1 1,然后两两归并,得到 ⌈ n / 2 ⌉ ⌈n/2⌉ n/2个长度为 2 2 2 1 1 1的有序表;继续两两…如此重复,直到合并成一个长度为 n n n的有序表为止,这种排序方法称为 2 2 2路归并排序。
    下图所示为 2 2 2路归并排序的一个例子,经过三趟归并后合并成了有序序列。
    在这里插入图片描述
    Merge()的功能是将前后相邻的两个有序表归并为一个有序表。设两段有序表 A [ l o w . . . m i d ] A[low...mid] A[low...mid] A [ m i d + 1... h i g h ] A [mid+1...high] A[mid+1...high]存放在同一顺序表中的相邻位置,先将它们复制到辅助数组 B B B中。每次从对应 B B B中的两个段取出一个记录进行关键字的比较,将较小者放入 A A A中,当数组 B B B中有一段的下标超出其对应的表长(即该段的所有元素都已复制到 A A A中)时,将另一段中的剩余部分直接复制到 A A A中。算法如下:

    ElemType *B = (ElemType *)malloc((n+1)*sizeof(ElemType));	//辅助数组B
    void Merge(ElemType A[], int low, int mid, int high){
    	//表A的两段A[low...mid]和A[mid+1...high]各自有序,将它们合并成一个有序表
    	for(int k=low; k<=high; k++){
    		B[k] = A[k];	//将A中所有元素复制到B中
    	}
    	for(i=low, j=mid+1, k=i; i<=mid && j<=high; k++){
    		//从low到mid,j从mid+1到high,k是最终排序数组的下标
    		if(B[i] <= B[j]){	//比较B左右两段的元素
    			A[k] = B[i++];	//将较小值赋值给A,B左段下标加1,右段不动
    		}else{
    			A[k] = B[j++];	//将较小值赋值给A,B右段下标加1,左段不动
    		}
    	}
    	while(i <= mid){	
    		//若第一个表(左段)未检测完,复制
    		A[k++] = B[i++];
    	}
    	while(j <= high){	
    		//若第二个表(右段)未检测完,复制
    		A[k++] = B[j++];
    	}
    }
    

    一趟归并排序的操作是,调用 ⌈ n / 2 h ⌉ ⌈n/2h⌉ n/2h次算法 m e r g e ( ) merge() merge(),将 L [ 1... n ] L[1...n] L[1...n]中前后相邻且长度为 h h h的有序段进行两两归并,得到前后相邻、长度为 2 h 2h 2h的有序段,整个归并排序需要进行 ⌈ l o g 2 n ⌉ ⌈log_2n⌉ log2n趟。递归形式的 2 2 2路归并排序算法是基于分治的,其过程如下。
    分解:将含有 n n n个元素的待排序表分成各含 n / 2 n/2 n/2个元素的子表,采用 2 2 2路归并排序算法对两个子表递归地进行排序。
    合并:合并两个已排序的子表得到排序结果。

    viod MergeSort(ElemType A[], int low, int high){
    	if(low < high){
    		int mid = (low + high)/2;	//从中间划分两个子序列
    		MergeSort(A, low, mid);	//对左侧子序列进行递归排序
    		MergeSort(A, mid+1, high);	//对右侧子序列进行递归排序
    		Merge(A, low, mid, high);	//归并
    	}
    }
    

    用图像描述,整个的递归拆分加归并排序的过程大概如下:
    在这里插入图片描述

    2、性能分析

    空间效率: Merge()操作中,辅助空间刚好为 n n n个单元,所以算法的空间复杂度为 O ( n ) O(n) O(n)
    时间效率:每趟归并的时间复杂度为 O ( n ) O(n) O(n),共需进行 ⌈ l o g 2 n ⌉ ⌈log_2n⌉ log2n趟归并,所以算法的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
    稳定性:由于Merge()操作不会改变相同关键字记录的相对次序,所以2路归并排序算法是一种稳定的排序方法。


    八、快速排序

    快速排序算法,被列为20世纪十大算法之一。快速排序是所有内部排序算法中平均性能最优的排序算法。

    前面介绍及几种算法中,希尔排序相当于直接插入排序的升级,它们同属于插入排序类,堆排序相当于简单选择排序的升级,它们同属于选择排序类。而快速排序是冒泡排序的升级,它们都属于交换排序类。即它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动交换次数。

    快速排序(Quick Sort) 的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小, 则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

    1、算法

    假设关键字数组为 { 49 , 38 , 65 , 97 , 76 , 13 , 27 } \{49,38,65,97,76,13,27\} {49,38,65,97,76,13,27}。快速排序算法的核心逻辑就是先选取当中的一个关键字,比如选择第一个关键字49,然后想尽办法将它放到一个位置,使得它左边的值都比它小,右边的值比它大,我们将这样的关键字称为枢轴(pivot)

    快速排序的基本思想是基于分治法的:在待排序表 L [ 1... n ] L[1...n] L[1...n]中任取一个元素 p i v o t pivot pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分 L [ 1... k − 1 ] L[1...k-1] L[1...k1] L [ k + 1... n ] L[k+1...n] L[k+1...n],使得 L [ 1... k − 1 ] L[1...k-1] L[1...k1]中的所有元素小于 p i v o t pivot pivot L [ k + 1... n ] L[k+1...n] L[k+1...n]中的所有元素大于等于 p i v o t pivot pivot,则 p i v o t pivot pivot放在了其最终位置 L ( k ) L(k) L(k)上,这个过程称为一趟快速排序(或一次划分)。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
    一趟快速排序的过程是一个交替搜索和交换的过程,下面通过实例来介绍,附设两个指针 i i i j j j,初值分别为 l o w low low h i g h high high,取第一个元素 49 49 49为枢轴赋值到变量 p i v o t pivot pivot
    指针j从 h i g h high high往前搜索找到第一个小于枢轴的元素 27 27 27,将 27 27 27交换到 i i i所指位置。
    在这里插入图片描述
    指针 i i i l o w low low往后搜索找到第一个大于枢轴的元素 65 65 65,将 65 65 65交换到 i i i所指位置。
    在这里插入图片描述
    指针 j j j继续往前搜索找到小于枢轴的元素 13 13 13,将 13 13 13交换到i所指位置。
    在这里插入图片描述
    指针 i i i继续往后搜索找到大于枢轴的元素 97 97 97,将 97 97 97交换到j所指位置。
    在这里插入图片描述
    指针j继续往前搜索小于枢轴的元素,直至 i = = j i==j i==j
    在这里插入图片描述
    此时 i = = j i==j i==j,指针 i i i之前的元素均小于 49 49 49,指针 i i i之后的元素均大于等于 49 49 49,将 49 49 49放在 i i i所指位置即其最终位置,经过一趟划分, 将原序列分割成了前后两个子序列。
    在这里插入图片描述
    按照同样的方法对各子序列进行快速排序,若待排序列中只有一个元素,显然已有序。
    在这里插入图片描述
    代码实现大致如下:

    void QuickSort(ElemType A[], int low, int high){
    	if(low < high){
    		//Partition()就是划分操作,将表A[low...high]划分为满足上述条件的两个子表
    		int pivotpos = Partition(A, low, high);	//划分
    		QuickSort(A, low, pivotpos-1);	//依次对两个子表进行递归操作
    		QuickSort(A, pivotpos+1, high);
    	}
    }
    

    假设每次总以当前表中第一个元素作为枢轴来对表进行划分,则将表中比枢轴大的元素向右移动,将比枢轴小的元素向左移动,使得一趟Partition ()操作后,表中的元素被枢轴值一分为二。代码如下:

    int Partition(ElemType A[], int low, int high){
    	ElemType pivot = A[low];	//将当前表中第一个元素设为枢轴,对表进行划分
    	while(low < high){
    		while(low < high && A[high] >= pivot){
    			--high;	
    		}
    		A[low] = A[high]	//将比枢轴小的元素移动到左端
    		while(low < high && A[low] <= pivot){
    			++low;	
    		}
    		A[high] = A[low];	//将比枢轴大的元素移动到右端
    	}
    	A[low] = pivot;	//枢轴元素存放到最终位置
    	return low;	//返回存放枢轴的最终位置
    }
    

    2、性能分析

    空间效率:由于快速排序是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量应与递归调用的最大深度一致。最好情况下为
    O ( l o g 2 n ) O(log_2n) O(log2n);最坏情况下,因为要进行 n − 1 n-1 n1次递归调用,所以栈的深度为 O ( n ) O(n) O(n);平均情况下,栈的深度为 O ( l o g 2 n ) O(log_2n) O(log2n)
    时间效率:快速排序的运行时间与划分是否对称有关,快速排序的最坏情况发生在两个区域分别包含 n − 1 n-1 n1个元素和 0 0 0个元素时,这种最大程度的不对称性若发生在每层递归上,即对应于初始排序表基本有序或基本逆序时,就得到最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    有很多方法可以提高算法的效率:一种方法是尽量选取一个可以将数据中分的枢轴元素,如从序列的头尾及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素;或者随机地从当前表中选取枢轴元素,这样做可使得最坏情况在实际排序中几乎不会发生。
    在最理想的状态下,即Partition ()可能做到最平衡的划分,得到的两个子问题的大小都不可能大于 n / 2 n/2 n/2,在这种情况下,快速排序的运行速度将大大提升,此时,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。 好在快速排序平均情况下的运行时间与其最佳情况下的运行时间很接近,而不是接近其最坏情况下的运行时间。快速排序是所有内部排序算法中平均性能最优的排序算法。
    稳定性:在划分算法中,若右端区间有两个关键字相同,且均小于基准值的记录,则在交换到左端区间后,它们的相对位置会发生变化,即快速排序是一种不稳定的排序方法。

    各种排序算法的比较

    在这里插入图片描述

    附录

    上文链接

    数据结构:查找

    专栏

    数据结构专栏

    参考资料

    1、严蔚敏、吴伟民:《数据结构(C语言版)》
    2、程杰:《大话数据结构》
    3、王道论坛:《数据结构考研复习指导》
    4、托马斯·科尔曼等人:《算法导论》

    展开全文
  • 本文代码实现基本按照《数据结构》课本目录顺序,外加大量的复杂算法实现,一篇文章足够。能换你一个收藏了吧?

    本文代码实现基本按照《数据结构》课本目录顺序,外加大量的复杂算法实现,一篇文章足够。能换你一个收藏了吧?

     当然如果落下什么了欢迎大家评论指出

    目录

    顺序存储线性表实现 

    单链表不带头标准c语言实现

    单链表不带头压缩c语言实现

    约瑟夫环-(数组、循环链表、数学) 

    线性表表示集合

     线性表实现一元多项式操作

    链表环问题

     

    移除链表元素

    回文链表

    链表表示整数,相加

    LRU

    LFU

    合并链表

    反转链表

     反转链表2

    对链表排序

    旋转链表

     数组实现栈

    链表实现栈

    数组实现队列

    链表实现队列

    双栈的实现

     栈/队列 互相模拟实现

    栈的排序

    栈——括号匹配

    栈——表达式求值 

    借汉诺塔理解栈与递归

    单调栈

    双端单调队列

     单调队列优化的背包问题

    01背包问题 

    完全背包问题 

    多重背包问题 

     串的定长表示

    串的堆分配实现

    KMP

    一、引子

    二、分析总结

    三、基本操作

    四、原理

    五、复杂度分析

    Manacher

    小问题一:请问,子串和子序列一样么?请思考一下再往下看

    小问题二:长度为n的字符串有多少个子串?多少个子序列?

    一、分析枚举的效率

    二、初步优化

    问题三:怎么用对称轴向两边扩的方法找到偶回文?(容易操作的)

    那么请问,加进去的符号,有什么要求么?是不是必须在原字符中没出现过?请思考

    小结:

    三、Manacher原理

    假设遍历到位置i,如何操作呢

    四、代码及复杂度分析

    前缀树

    后缀树/后缀数组

    后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。

     

    相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。

    后缀数组:就是把某个字符串的所有后缀按照字典序排序后的数组。(数组中保存起始位置就好了,结束位置一定是最后)

    AC自动机

    数组缺失

    二叉树遍历

    前序

    中序

    后序

    进一步思考

    二叉树序列化/反序列化

    先序中序后序两两结合重建二叉树

    先序遍历

    中序遍历

    后序遍历

    层次遍历

    输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

    输入某二叉树的后序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

    输入某二叉树的后序遍历和先序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

    先序中序数组推后序数组

    二叉树遍历

    遍历命名

    方法1:我们可以重建整棵树:

    https://blog.csdn.net/hebtu666/article/details/84322113

    方法2:我们可以不用重建,直接得出:

    根据数组建立平衡二叉搜索树

    java整体打印二叉树

    判断平衡二叉树

    判断完全二叉树

    判断二叉搜索树

    二叉搜索树实现

    堆的简单实现

    堆应用例题三连

    一个数据流中,随时可以取得中位数。

    金条

    项目最大收益(贪心问题)

     并查集实现

    并查集入门三连:HDU1213 POJ1611 POJ2236

    HDU1213

    POJ1611

     POJ2236

    线段树简单实现

    功能:一样的,依旧是查询和改值。

    查询[s,t]之间最小的数。修改某个值。

    那我们继续说,如何查询。

    如何更新?

     树状数组实现

    最大搜索子树

    morris遍历

    最小生成树

    拓扑排序

    最短路

     

    简单迷宫问题

    深搜DFS\广搜BFS 

     皇后问题

    一般思路:

    优化1:

    优化2:

    二叉搜索树实现

    Abstract Self-Balancing Binary Search Tree

     

    二叉搜索树

    概念引入

    AVL树

    红黑树

    size balance tree

    伸展树

    Treap

    最简单的旋转

    带子树旋转

    代码实现

    AVL Tree

    前言

    二叉搜索树

    AVL Tree

    旋转

    旋转总结

    单向右旋平衡处理LL:

    单向左旋平衡处理RR:

    双向旋转(先左后右)平衡处理LR:

    双向旋转(先右后左)平衡处理RL:

    深度的记录

    单个节点的深度更新

    写出旋转代码

    总写调整方法

    插入完工

    删除

    直观表现程序

    跳表介绍和实现

    c语言实现排序和查找所有算法

     

     


    顺序存储线性表实现 

    在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。

     

    顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。

    优点:随机存取表中元素。缺点:插入和删除操作需要移动元素。

     

    线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)。

    给出两种基本实现:

    /*
    静态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INITSIZE 100
    #define ElemType int
    #define Status int
    #define OK     1
    #define ERROR  0
    
    typedef struct
    {
    	ElemType elem[LIST_INITSIZE];
    	int length;
    }SqList;
    
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DisCreat(SqList A,SqList *B,SqList *C);//拆分(按正负),也可以根据需求改
    //虽然思想略简单,但是要写的没有错误,还是需要锻炼coding能力的
    
    Status InitList(SqList *L)
    {
        L->length = 0;//长度为0
        return OK;
    }
    
    Status ListInsert(SqList *L, int i,ElemType e)
    {
        int j;
        if(i<1 || i>L->length+1)
            return ERROR;//判断非法输入
        if(L->length == LIST_INITSIZE)//判满
        {
            printf("表已满");//提示
            return ERROR;//返回失败
        }
        for(j = L->length;j > i-1;j--)//从后往前覆盖,注意i是从1开始
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        (L->length)++;//表长加1
        return OK;//反回成功
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i = 0;i < L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DisCreat(SqList A,SqList *B,SqList *C)
    {
        int i;
        for(i = 0;i < A.length;i++)//依次遍历A中元素
        {
            if(A.elem[i]<0)//判断
                ListInsert(B,B->length+1,A.elem[i]);//直接调用插入函数实现尾插
            else
                ListInsert(C,C->length+1,A.elem[i]);
        }
    }
    
    int main(void)
    {
        //复制的
    	SqList L;
    	SqList B, C;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,33,-3,-88,21,77,0,-9};
    	InitList(&L);
    	InitList(&B);
    	InitList(&C);
    	for (i = 1; i <= 9; i++)
    		ListInsert(&L,i,data[i-1]);
        printf("插入完成后L = : ");
    	ListPrint(L);
        ListDelete(&L,1,&e);
    	printf("删除第1个后L = : ");
    	ListPrint(L);
        DisCreat(L , &B, &C);
    	printf("拆分L后B = : ");
    	ListPrint(B);
    	printf("拆分L后C = : ");
    	ListPrint(C);
    	printf("拆分L后L = : ");
    	ListPrint(L);
    }

    静态:长度固定

    动态:不够存放可以加空间(搬家)

     

    /*
    子任务名任务:1_2 动态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INIT_SIZE 100
    #define LISTINCREMENT 10
    #define Status int
    #define OVERFLOW -1
    #define OK 1
    #define ERROR 0
    #define ElemType int
    
    typedef struct
    {
    	ElemType * elem;
    	int length;
    	int listsize;
    }SqList;
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DeleteMin(SqList *L);//删除最小
    
    Status InitList(SqList *L)
    {
        L->elem = (ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));//申请100空间
    	if(!L->elem)//申请失败
    		return ERROR;
    	L->length = 0;//长度0
    	L->listsize = LIST_INIT_SIZE;//容量100
    	return OK;//申请成功
    }
    
    Status ListInsert(SqList *L,int i,ElemType e)
    {
        int j;
        ElemType *newbase;
        if(i<1 || i>L->length+1)
            return ERROR;//非法输入
            
        if(L->length >= L->listsize)//存满了,需要更大空间
        {
            newbase = (ElemType*)realloc(L->elem,(L->listsize+LISTINCREMENT)*sizeof(ElemType));//大10的空间
            if(!newbase)//申请失败
                return ERROR;
            L->elem = newbase;//调指针
            L->listsize+= LISTINCREMENT;//新容量
        }
        
        for(j=L->length;j>i-1;j--)//从后往前覆盖
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        L->length++;//长度+1
        return OK;
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i=0;i<L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DeleteMin(SqList *L)
    {
        //表空在Listdelete函数里判断
        int i;
        int j=0;//最小值下标
        ElemType *e;
        for(i=0;i<L->length;i++)//寻找最小
        {
            if(L->elem[i] < L->elem[j])
                j=i;
        }
        ListDelete(L,j+1,&e);//调用删除,注意j要+1
    }
    
    int main(void)
    {
    	SqList L;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,-33,3,-88,21,77,0,-9};
    	InitList(&L);
    	for (i = 1; i <= 9; i++)
    	{
    		ListInsert(&L,i,data[i-1]);
    	}
    	printf("插入完成后 L = : ");
    	ListPrint(L);
        ListDelete(&L, 2, &e);
    	printf("删除第 2 个后L = : ");
    	ListPrint(L);
        DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    }

    单链表不带头标准c语言实现

     

    链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

    使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表双向链表以及循环链表

     

    下面给出不带头的单链表标准实现:

    定义节点:

    typedef struct node 
    { 
        int data;
        struct node * next;
    }Node;

    尾插:

    void pushBackList(Node ** list, int data) 
    { 
        Node * head = *list;
        Node * newNode = (Node *)malloc(sizeof(Node));//申请空间
        newNode->data = data; newNode->next = NULL;
        if(*list == NULL)//为空
            *list = newNode;
        else//非空
        {
            while(head ->next != NULL)
                head = head->next;
            head->next = newNode;
        }
    }
    
    

    插入:

    int insertList(Node ** list, int index, int data) 
    {
        int n;
        int size = sizeList(*list); 
        Node * head = *list; 
        Node * newNode, * temp;
        if(index<0 || index>size) return 0;//非法
        newNode = (Node *)malloc(sizeof(Node)); //创建新节点
        newNode->data = data; 
        newNode->next = NULL;
        if(index == 0) //头插
        {
            newNode->next = head; 
            *list = newNode; 
            return 1; 
        }
        for(n=1; n<index; n++) //非头插
            head = head->next;
        if(index != size) 
            newNode->next = head->next; 
        //链表尾部next不需指定
        head->next = newNode; 
        return 1;
    }
    

    按值删除:

    void deleteList(Node ** list, int data) 
    { 
        Node * head = *list; Node * temp; 
        while(head->next!=NULL) 
        { 
            if(head->next->data != data) 
            { 
                head=head->next; 
                continue; 
            } 
            temp = head->next;
            if(head->next->next == NULL) //尾节点删除
                head->next = NULL; 
            else 
                head->next = temp->next; 
            free(temp);
        }    
        head = *list; 
        if(head->data == data) //头结点删除
        { 
            temp = head; 
            *list = head->next; 
            head = head->next; 
            free(temp); 
        }
    }
    

    打印:

    void printList(Node * head) 
    { 
        Node * temp = head; 
        for(; temp != NULL; temp=temp->next) 
            printf("%d ", temp->data); 
        printf("\n"); 
    }

    清空:

    void freeList(Node ** list) 
    { 
        Node * head = *list; 
        Node * temp = NULL; 
        while(head != NULL) //依次释放
        { 
            temp = head; 
            head = head->next; 
            free(temp); 
        } 
        *list = NULL; //置空
    }

    别的也没啥了,都是基本操作

    有些代码要分情况,很麻烦,可读性较强吧

     

    单链表不带头压缩c语言实现

     

     

     注:单追求代码简洁,所以写法可能有点不标准。

    //第一次拿c开始写数据结构,因为自己写的,追求代码量少,和学院ppt不太一样。有错请指出
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    typedef struct node//定义节点
    {
        int data;
        struct node * next;
    }Node;

     

    //函数介绍
    void printlist(Node * head)//打印链表
    int lenlist(Node * head)//返回链表长度
    void insertlist(Node ** list,int data,int index)//插入元素
    void pushback(Node ** head,int data)//尾部插入
    void freelist(Node ** head)//清空链表
    void deletelist(Node ** list,int data)//删除元素
    Node * findnode(Node ** list,int data)//查找
    void change(Node ** list,int data,int temp)//改变值

    打印

    void printlist(Node * head)//打印链表
    {
        for(;head!=NULL;head=head->next) printf("%d ",head->data);
        printf("\n");//为了其他函数打印,最后换行
    }

    链表长度

    int lenlist(Node * head)//返回链表长度
    {
        int len;
        Node * temp = head;
        for(len=0; temp!=NULL; len++) temp=temp->next;
        return len;
    }

    插入元素

    void insertlist(Node ** list,int data,int index)//插入元素,用*list将head指针和next统一表示
    {
        if(index<0 || index>lenlist(*list))return;//判断非法输入
        Node * newnode=(Node *)malloc(sizeof(Node));//创建
        newnode->data=data;
        newnode->next=NULL;
        while(index--)list=&((*list)->next);//插入
        newnode->next=*list;
        *list=newnode;
    }

    尾部增加元素

    void pushback(Node ** head,int data)//尾插,同上
    {
        Node * newnode=(Node *)malloc(sizeof(Node));//创建
        newnode->data=data;
        newnode->next=NULL;
        while(*head!=NULL)head=&((*head)->next);//插入
        *head=newnode;
    }

    清空链表

    void freelist(Node ** head)//清空链表
    {
        Node * temp=*head;
        Node * ttemp;
        *head=NULL;//指针设为空
        while(temp!=NULL)//释放
        {
            ttemp=temp;
            temp=temp->next;
            free(ttemp);
        }
    }

    删除

    void deletelist(Node ** list,int data)//删除链表节点
    {
        Node * temp;//作用只是方便free
        while((*list)->data!=data && (*list)->next!=NULL)list=&((*list)->next);
        if((*list)->data==data){
            temp=*list;
            *list=(*list)->next;
            free(temp);
        }
    }

    查找

    Node * findnode(Node ** list,int data)//查找,返回指向节点的指针,若无返回空
    {
        while((*list)->data!=data && (*list)!=NULL) list=&((*list)->next);
        return *list;
    }

    改值

    void change(Node ** list,int data,int temp)//改变
    {
        while((*list)->data!=data && (*list)->next!=NULL)list=&((*list)->next);
        if((*list)->data==data)(*list)->data=temp;
    }

     

    最后测试

    int main(void)//测试
    {
        Node * head=NULL;
        Node ** gg=&head;
        int i;
        for(i=0;i<10;i++)pushback(gg,i);
        printf("链表元素依次为: ");
        printlist(head);
        printf("长度为%d\n",lenlist(head));
        freelist(gg);
        printf("释放后长度为%d\n",lenlist(head));
        for(i=0;i<10;i++)pushback(gg,i);
        deletelist(gg,0);//头
        deletelist(gg,9);//尾
        deletelist(gg,5);
        deletelist(gg,100);//不存在
        printf("再次创建链表,删除节点后\n");
        printlist(head);
        freelist(gg);
        for(i=0;i<5;i++)pushback(gg,i);
        insertlist(gg,5,0);//头
        insertlist(gg,5,5);
        insertlist(gg,5,7);//尾
        insertlist(gg,5,10);//不存在
        printlist(head);
        printf("找到%d\n把3变为100",*findnode(gg,5));
        change(gg,3,100);
        change(gg,11111,1);//不存在
        printlist(head);
    }
    

    约瑟夫环-(数组、循环链表、数学) 

    约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依规律重复下去,直到圆桌周围的人全部出列。

     

    约瑟夫环运作如下:

    1、一群人围在一起坐成环状(如:N)

    2、从某个编号开始报数(如:S)

    3、数到某个数(如:M)的时候,此人出列,下一个人重新报数

    4、一直循环,直到所有人出列  ,约瑟夫环结束

    模拟过程,求出最后的人。

    把数组看成一个环,从第s个元素开始按m-1间隔删除元素,重复过程,直到元素全部去掉。

     

    void Josephus(int a[],int n,int m,int s)
    {
        int i,j;
        int k=n;
        for(i=0;i<n;i++)a[i]=i+1;//编号
        i=(s+n-1)%n;
        while(k)
        {
            for(j=1;j<m;j++)i=(i+1)%k;//依次报数,头尾相连
            printf("%d\n",a[i]);//出局
            for(j=i+1;j<k;j++)a[j-1]=a[j];//删除本节点
            k--;
        }
        //模拟结束,最后输出的就是留下的人
    }

     

    可以用带头单循环链表来求解:

    也是一样的,只是实现不同,给出核心代码:

        while(k)
        {
            for(j=1;j<m;j++)
            {
                pr=p;
                p=p->link;
                if(p==head)//头结点跳过
                {
                    pr=p;
                    p=p->link;
                }
            }
            k--;
            //打印
            pr->link=p->link;//删结点
            free(p);
            p=pr->link;//从下一个继续
        }

    双向循环链表也可以解,和单链表类似,只是不需要保持前趋指针。

     

    数学可解:

    效率最高

    
    int check_last_del(int n,int m)
    {
    	int i = 1;
    	int ret = 0;
    	for (i = 2; i<=n;i++)
            ret = (ret + m) %i;
    	return ret+1;//因为ret是从0到n-1,最后别忘了加1。
    }
    

    线性表表示集合

    集合我们高中都学过吧?

    最重要的几个特点:元素不能重复、各个元素之间没有关系、没有顺序

    集合内的元素可以是单元素或者是集合。

    对集合的操作:交集并集差集等,还有对自身的加减等。

    需要频繁的加减元素,所以顺序存储效率较低,但是我们还是说一下是怎么实现的:

        用01向量表示集合,因为现实中任何一个有穷集合都能对应到一个0、1、2.....n这么一个序列中。所以可以对应过来,每位的01代表这个元素存在与否即可。

    链接存储表示使用有序链表来实现,虽然集合是无序的,但是我们的链表可以是有序的。可以按升序排列。而链表理论上可以无限增加,所以链表可以表示无限集。

    下面我们来实现一下:

    我们定义一个节点:

    typedef int ElemType;
    typedef struct SetNode{//节点定义
        ElemType data;//数据
        struct SetNode * link;
    }*LinkedSet//集合定义

    然后要实现那些操作了,首先想插入吧:我们对于一个新元素,查找集合中是否存在,存在就不插入,不存在就插入到查找失败位置。

    删除也简单,查找存在就删除。

     

    我们说两个集合的操作:

    求两个集合的并:

    两个链表,都是升序。把他们去重合并即可。

    其实和链表归并的merge过程是一样的,只是相等的时候插入一个,两个指针都向后走就行了。

    我就再写一遍吧。

    void UnionSet(LinkedSet & A,LinkedSet & B,LinkedSet & C)
    {
        SetNode *pa=A->link,*pb=B->link,*pc=C;
        while(pa && pb)//都不为空
        {
            if(pa->data==pb->data)//相等,插一次,两边向后
            {
                pc->link=new SetNode;
                pc->data=pa->data;
                pa=pa->link;
                pb=pb->link;
            }
            else if(pa->data<pb->data)//插小的,小的向后
            {
                pc->link=new SetNode;
                pc->data=pa->data;
                pa=pa->link;
            }
            else
            {
                pc->link=new SetNode;
                pc->data=pb->data;
                pb=pb->link;
            }
            pc=pc->link;//注意指针
        }
        if(pa)p=pa;//剩下的接上
        else p=pb;//只执行一个
        while(p)//依次复制
        {
            pc->link=new SetNode;
            pc->data=p->data;
            pc=pc->link;
            p=p->link;
        }
        pc->link=NULL;
    }

    求两个集合的交,更简单,还是这三种情况,谁小谁向后,相等才插入。

    void UnionSet(LinkedSet & A,LinkedSet & B,LinkedSet & C)
    {
        SetNode *pa=A->link,*pb=B->link,*pc=C;
        while(pa && pb)//都不为空
        {
            if(pa->data==pb->data)//相等,插一次,两边向后
            {
                pc->link=new SetNode;
                pc->data=pa->data;
                pa=pa->link;
                pb=pb->link;
                pc=pc->link;//注意指针,就不是每次都向后了,只有插入才向后
            }
            else if(pa->data<pb->data)//小的向后
            {
                pa=pa->link;
            }
            else
            {
                pb=pb->link;
            }
        }
        pc->link=NULL;
    }

    求两个集合的差:高中可能没学这个概念,其实就是A-B,就是B中的元素,A都不能有了。

    运算你可以把B元素全过一遍,A中有就去掉,但是这样时间复杂度太高了,我们需要O(A+B)而不是O(A*B)

    因为有序,很好操作,还是两个指针,

    如果AB相同,都向后移。

    或者,B小,B就向后移。

    如果A小,说明B中不含这个元素,我们把它复制到结果链表里。

     

    思想还行,实在懒得写了,有时间再说吧。

     线性表实现一元多项式操作

     

    数组存放:

    不需要记录幂,下标就是。

    比如1,2,3,5表示1+2x+3x^2+5x^3

    有了思路,我们很容易定义结构

    typedef struct node{
        float * coef;//系数数组
        int maxSize;//最大容量
        int order;//最高阶数
    }Polynomial;

    先实现求和:我们想求两个式子a+b,结果存在c中。

    逻辑很简单,就是相加啊。

    void Add(Polynomial & A,Polynomial & B,Polynomial & C)
    {
        int i;
        int m=A.order;
        int n=B.order;
        for(i=0;i<=m && i<=n;i++)//共有部分加一起
            C.coef[i]=A.coef[i]+B.coef[i];
        while(i<=m)//只会执行一个,作用是把剩下的放入c
            C.coef[i]=A.coef[i];
        while(i<=n)
            C.coef[i]=B.coef[i];
        C.order=(m>n)?m:n;//等于较大项
    }

    实现乘法:

    我们思考一下,两个多项式怎么相乘?

    把a中每一项都和b中每一项乘一遍就好了。

    高中知识

     

    void Mul(Polynomial & A,Polynomial & B,Polynomial & C)
    {
        int i;
        int m=A.order;
        int n=B.order;
        if(m+n>C.maxSize)
        {
            printf("超限");
            return;
        }
        for(i=0;i<=m+n;i++)//注意范围,是最高项的幂加起来
            C.coef[i]=0.0;
        for(i=0;i<=m;i++)
        {
            for(j=0;j<=n;j++)
            {
                C.coef[i+j]+=A.coef[i]*B.coef[j];
            }
        }
        C.order=m+n;//注意范围,是最高项的幂加起来
    }

     

    利用数组存放虽然简单,但是当幂相差很大时,会造成空间上的严重浪费(包括时间也是),所以我们考虑采用链表存储。

     

    我们思考一下如何存储和做运算。

     

    我们肯定要再用一个变量记录幂了。每个节点记录系数和指数。

    考虑如何相加:

    对于c,其实刚开始是空的,我们首先要实现一个插入功能,然后,遍历a和b,进一步利用插入函数来不断尾插。

    因为a和b都是升幂排列,所以相加的时候,绝对不会发生结果幂小而后遇到的情况,所以放心的一直插入就好了。

    具体实现也比较好想:a和b幂相等就加起来,不等就小的单独插入,然后指针向后移。

    加法就放老师写的代码吧,很漂亮的代码:(没和老师商量,希望不会被打)

    老师原地插的,都一样都一样

    老师原文:http://www.edu2act.net/article/shu-ju-jie-gou-xian-xing-biao-de-jing-dian-ying-yong/

    void AddPolyn(polynomial &Pa, polynomial &Pb)
    	//多项式的加法:Pa = Pa + Pb,利用两个多项式的结点构成“和多项式”。 
    {
    	LinkList ha = Pa;		//ha和hb分别指向Pa和Pb的头指针
    	LinkList hb = Pb;
    	LinkList qa = Pa->next;
    	LinkList qb = Pb->next;	//ha和hb分别指向pa和pb的前驱
    	while (qa && qb)		//如果qa和qb均非空
    	{
    		float sum = 0.0;
    		term a = qa->data;
    		term b = qb->data;
    		switch (cmp(a,b))
    		{
    		case -1:	//多项式PA中当前结点的指数值小
    			ha = qa;
    			qa = qa->next;
    			break;
    		case 0:		//两者指数值相等
    			sum = a.coef + b.coef;
    			if(sum != 0.0)
    			{	//修改多项式PA中当前结点的系数值
    				qa->data.coef = sum;
    				ha = qa;
    			}else
    			{	//删除多项式PA中当前结点
    				DelFirst(ha, qa);
    				free(qa);
    			}
    			DelFirst(hb, qb);
    			free(qb);
    			qb = hb->next;
    			qa = ha->next;
    			break;
    		case 1:
    			DelFirst(hb, qb);
    			InsFirst(ha, qb);
    			qb = hb->next;
    			ha = ha->next;
    			break;
    		}//switch
    	}//while
    	if(!ListEmpty(Pb))
    		Append(Pa,qb);
    	DestroyList(hb);
    
    }//AddPolyn

    对于乘法,我们就不能一直往后插了,因为遍历两个式子,可能出现幂变小的情况。所以我们要实现一个插入函数,如果c中有这一项,就加起来,没这一项就插入。

    我们先实现插入函数:(哦,对了,我没有像老师那样把系数和指数再定义一个结构体,都放一起了。还有next我写的link,还有点别的不一样,都无伤大雅,绝对能看懂)

    void Insert(Polynomial &L,float c,int e)//系数c,指数e
    {
        Term * pre=L;
        Term * p=L->link;
        while(p && p->exp<e)//查找
        {
            pre=p;
            p=p->link;
        }
        if(p->exp==e)//如果有这一项
        {
            if(p->coef+c)//如果相加是0了,就删除节点
            {
                pre->link=p->link;
                free(p);
            }
            else//相加不是0,就合并
            {
                p->coef+=c;
            }
        }
        else//如果没这一项,插入就好了,链表插入写了很多遍了
        {
                Term * pc=new Term;//创建
                pc->exp=e;
                pc->coef=c;
                pre->link=pc;
                pc->link=p;        
        }
    }

    插入写完了,乘法就好实现了,还是两个循环,遍历a和b,只是最后调用Insert方法实现就ok

    insert(c,乘系数,加幂)

     

    拓展:一维数组可以模拟一元多项式。类似的,二维数组可以模拟二元多项式。实现以后有时间写了再放链接。

     

     

    链表环问题

    1.判断单链表是否有环

      使用两个slow, fast指针从头开始扫描链表。指针slow 每次走1步,指针fast每次走2步。如果存在环,则指针slow、fast会相遇;如果不存在环,指针fast遇到NULL退出。

      就是所谓的追击相遇问题:

        

    2.求有环单链表的环长

       在环上相遇后,记录第一次相遇点为Pos,之后指针slow继续每次走1步,fast每次走2步。在下次相遇的时候fast比slow正好又多走了一圈,也就是多走的距离等于环长。

      设从第一次相遇到第二次相遇,设slow走了len步,则fast走了2*len步,相遇时多走了一圈:

        环长=2*len-len。

    3.求有环单链表的环连接点位置

      第一次碰撞点Pos到连接点Join的距离=头指针到连接点Join的距离,因此,分别从第一次碰撞点Pos、头指针head开始走,相遇的那个点就是连接点。

         

      在环上相遇后,记录第一次相遇点为Pos,连接点为Join,假设头结点到连接点的长度为LenA,连接点到第一次相遇点的长度为x,环长为R

        第一次相遇时,slow走的长度 S = LenA + x;

        第一次相遇时,fast走的长度 2S = LenA + n*x;

        所以可以知道,LenA + x =  n*R;  LenA = n*R -x;

    4.求有环单链表的链表长

       上述2中求出了环的长度;3中求出了连接点的位置,就可以求出头结点到连接点的长度。两者相加就是链表的长度。

     

    编程实现:

      下面是代码中的例子:

      

      具体代码如下:

    #include <stdio.h>
    #include <stdlib.h>
    typedef struct node{
        int value;
        struct node *next;
    }LinkNode,*Linklist;
    
    /// 创建链表(链表长度,环节点起始位置)
    Linklist createList(){
        Linklist head = NULL;
        LinkNode *preNode = head;
        LinkNode *FifthNode = NULL;
        for(int i=0;i<6;i++){
            LinkNode *tt = (LinkNode*)malloc(sizeof(LinkNode));
            tt->value = i;
            tt->next = NULL;
            if(preNode == NULL){
                head = tt;
                preNode = head;
            }
            else{
                preNode->next =tt;
                preNode = tt;
            }
    
            if(i == 3)
                FifthNode = tt;
        }
        preNode->next = FifthNode;
        return head;
    }
    
    ///判断链表是否有环
    LinkNode* judgeRing(Linklist list){
        LinkNode *fast = list;
        LinkNode *slow = list;
    
        if(list == NULL)
            return NULL;
    
        while(true){
            if(slow->next != NULL && fast->next != NULL && fast->next->next != NULL){
                slow = slow->next;
                fast = fast->next->next;
            }
            else
                return NULL;
    
            if(fast == slow)
                return fast;
        }
    }
    
    ///获取链表环长
    int getRingLength(LinkNode *ringMeetNode){
        int RingLength=0;
        LinkNode *fast = ringMeetNode;
        LinkNode *slow = ringMeetNode;
        for(;;){
            fast = fast->next->next;
            slow = slow->next;
            RingLength++;
            if(fast == slow)
                break;
        }
        return RingLength;
    }
    
    ///获取链表头到环连接点的长度
    int getLenA(Linklist list,LinkNode *ringMeetNode){
        int lenA=0;
        LinkNode *fast = list;
        LinkNode *slow = ringMeetNode;
        for(;;){
            fast = fast->next;
            slow = slow->next;
            lenA++;
            if(fast == slow)
                break;
        }
        return lenA;
    }
    
    ///环起始点
    ///如果有环, 释放空空间时需要注意.
    LinkNode* RingStart(Linklist list, int lenA){
        if (!list || lenA <= 0){
            return NULL;
        }
    
        int i = 0;
        LinkNode* tmp = list;
        for ( ; i < lenA; ++i){
            if (tmp != NULL){
                tmp = tmp->next;
            }
        }
    
        return (i == lenA)? tmp : NULL;
    }
    
    ///释放空间
    int freeMalloc(Linklist list, LinkNode* ringstart){
        bool is_ringstart_free = false; //环起始点只能被释放空间一次
        LinkNode *nextnode = NULL;
    
        while(list != NULL){
            nextnode = list->next;
            if (list == ringstart){ //如果是环起始点
                if (is_ringstart_free)
                    break;  //如果第二次遇到环起始点addr, 表示已经释放完成
                else
                    is_ringstart_free = true;   //记录已经释放一次
            }
            free(list);
            list = nextnode;
        }
    
        return 0;
    }
    
    int main(){
        Linklist list = NULL;
        LinkNode *ringMeetNode  = NULL;
        LinkNode *ringStartNode = NULL;
    
        int LenA       = 0;
        int RingLength = 0;
    
        list = createList();
        ringMeetNode = judgeRing(list); //快慢指针相遇点
    
        if(ringMeetNode == NULL)
            printf("No Ring\n");
        else{
            printf("Have Ring\n");
            RingLength = getRingLength(ringMeetNode);   //环长
            LenA = getLenA(list,ringMeetNode);
    
            printf("RingLength:%d\n", RingLength);
            printf("LenA:%d\n", LenA);
            printf("listLength=%d\n", RingLength+LenA);
        }
    
        ringStartNode = RingStart(list, LenA);  //获取环起始点
        freeMalloc(list, ringStartNode);    //释放环节点, 有环时需要注意. 采纳5楼建议
        return 0;
    }

     

    移除链表元素

     

    删除链表中等于给定值 val 的所有节点。

    示例:

    输入: 1->2->6->3->4->5->6, val = 6
    输出: 1->2->3->4->5

    思路:就删呗,注意第一个数可能会被删

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
    	public ListNode removeElements(ListNode head, int val) {
    		ListNode p = new ListNode(-1);
    		p.next = head;
    		//因为要删除的可能是链表的第一个元素,所以用一个h节点来做处理
    		ListNode h = p;
    		while(p.next!=null) {
    			if(p.next.val==val) {
    				p.next = p.next.next;
    			}else{
                    p = p.next;
                }	
    		}
    		return h.next;
    	}
    }

    回文链表

    请判断一个链表是否为回文链表。

    示例 1:

    输入: 1->2
    输出: false
    示例 2:

    输入: 1->2->2->1
    输出: true
    进阶:
    你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

    思路:逆置前一半,然后从中心出发开始比较即可。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public boolean isPalindrome(ListNode head) {
            if(head == null || head.next == null) {
                return true;
            }
            ListNode slow = head, fast = head;
            ListNode pre = head, prepre = null;
            while(fast != null && fast.next != null) {
                pre = slow;
                slow = slow.next;
                fast = fast.next.next;
                pre.next = prepre;
                prepre = pre;
            }
            if(fast != null) {
                slow = slow.next;
            }
            while(pre != null && slow != null) {
                if(pre.val != slow.val) {
                    return false;
                }
                pre = pre.next;
                slow = slow.next;
            }
            return true;
        }
    }

    链表表示整数,相加

    思路:就模仿加法即可。。。题目还贴心的给把顺序反过来了。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
            ListNode ans=new ListNode(0);
            ListNode tempA=l1;
            ListNode tempB=l2;
            ListNode temp=ans;
            int out=0;
            while(tempA!=null || tempB!=null){
                int a=tempA!=null?tempA.val:0;
                int b=tempB!=null?tempB.val:0;
                ans.next=new ListNode((a+b+out)%10);
                ans=ans.next;
                out=(a+b+out)/10;
                if(tempA!=null)tempA=tempA.next;
                if(tempB!=null)tempB=tempB.next;
            }
            if(out!=0){
              ans.next=new ListNode(out);  
            }
            return temp.next;
        }
    }

    LRU

    LRU全称是Least Recently Used,即最近最久未使用的意思。

    LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。(这一段是找的,让大家理解一下什么是LRU)。

     

    说一下我们什么时候见到过LRU:其实老师们肯定都给大家举过这么个例子:你在图书馆,你把书架子里的书拿到桌子上。。但是桌子是有限的,你有时候不得不把一些书放回去。这就相当于内存和硬盘。这个例子都说过吧?

    LRU就是记录你最长时间没看过的书,就把它放回去。在cache那里见过吧

     

    然后最近在研究redis,又看到了这个LRU,所以就想写一下吧。

    题目:设计一个结构,这个结构可以查询K-V,但是容量有限,当存不下的时候就要把用的年代最久远的那个东西扔掉。

    其实思路很简单,我们维护一个双向链表即可,get也就是使用了,我们就把把它提到最安全的位置。新来的KV就依次放即可。

    我们就先写这个双向链表结构

    先写节点结构:

    	public static class Node<V> {
    		public V value;
    		public Node<V> last;//前
    		public Node<V> next;//后
    
    		public Node(V value) {
    			this.value = value;
    		}
    	}

    然后写双向链表结构: 我们没必要把链表操作都写了,分析一下,我们只有三个操作:

    1、加节点

    2、使用了某个节点就把它调到尾,代表优先级最高

    3、把优先级最低的移除,也就是去头部

    (不会的,翻我之前的链表操作都有写)

    	public static class NodeDoubleLinkedList<V> {
    		private Node<V> head;//头
    		private Node<V> tail;//尾
    
    		public NodeDoubleLinkedList() {
    			this.head = null;
    			this.tail = null;
    		}
    
    		public void addNode(Node<V> newNode) {
    			if (newNode == null) {
    				return;
    			}
    			if (this.head == null) {//头空
    				this.head = newNode;
    				this.tail = newNode;
    			} else {//头不空
    				this.tail.next = newNode;
    				newNode.last = this.tail;//注意让本节点前指针指向旧尾
    				this.tail = newNode;//指向新尾
    			}
    		}
    /*某个点移到最后*/
    		public void moveNodeToTail(Node<V> node) {
    			if (this.tail == node) {//是尾
    				return;
    			}
    			if (this.head == node) {//是头
    				this.head = node.next;
    				this.head.last = null;
    			} else {//中间
    				node.last.next = node.next;
    				node.next.last = node.last;
    			}
    			node.last = this.tail;
    			node.next = null;
    			this.tail.next = node;
    			this.tail = node;
    		}
    /*删除第一个*/
    		public Node<V> removeHead() {
    			if (this.head == null) {
    				return null;
    			}
    			Node<V> res = this.head;
    			if (this.head == this.tail) {//就一个
    				this.head = null;
    				this.tail = null;
    			} else {
    				this.head = res.next;
    				res.next = null;
    				this.head.last = null;
    			}
    			return res;
    		}
    
    	}

    链表操作封装完了就要实现这个结构了。

    具体思路代码注释

    	public static class MyCache<K, V> {
    		//为了kv or vk都能查
    		private HashMap<K, Node<V>> keyNodeMap;
    		private HashMap<Node<V>, K> nodeKeyMap;
    		//用来做优先级
    		private NodeDoubleLinkedList<V> nodeList;
    		private int capacity;//容量
    
    		public MyCache(int capacity) {
    			if (capacity < 1) {//你容量连1都不给,捣乱呢
    				throw new RuntimeException("should be more than 0.");
    			}
    			this.keyNodeMap = new HashMap<K, Node<V>>();
    			this.nodeKeyMap = new HashMap<Node<V>, K>();
    			this.nodeList = new NodeDoubleLinkedList<V>();
    			this.capacity = capacity;
    		}
    
    		public V get(K key) {
    			if (this.keyNodeMap.containsKey(key)) {
    				Node<V> res = this.keyNodeMap.get(key);
    				this.nodeList.moveNodeToTail(res);//使用过了就放到尾部
    				return res.value;
    			}
    			return null;
    		}
    
    		public void set(K key, V value) {
    			if (this.keyNodeMap.containsKey(key)) {
    				Node<V> node = this.keyNodeMap.get(key);
    				node.value = value;//放新v
    				this.nodeList.moveNodeToTail(node);//我们认为放入旧key也是使用过
    			} else {
    				Node<V> newNode = new Node<V>(value);
    				this.keyNodeMap.put(key, newNode);
    				this.nodeKeyMap.put(newNode, key);
    				this.nodeList.addNode(newNode);//加进去
    				if (this.keyNodeMap.size() == this.capacity + 1) {
    					this.removeMostUnusedCache();//放不下就去掉优先级最低的
    				}
    			}
    		}
    
    		private void removeMostUnusedCache() {
    			//删除头
    			Node<V> removeNode = this.nodeList.removeHead();
    			K removeKey = this.nodeKeyMap.get(removeNode);
    			//删除掉两个map中的记录
    			this.nodeKeyMap.remove(removeNode);
    			this.keyNodeMap.remove(removeKey);
    		}
    	}

    LFU

    请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。可以自行百度介绍,非常著名的结构

    实现 LFUCache 类:

    LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
    int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。
    void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。
    注意「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。

    为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。

    当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。


    你可以为这两种操作设计时间复杂度为 O(1) 的实现吗?

    // 缓存的节点信息
    struct Node {
        int key, val, freq;
        Node(int _key,int _val,int _freq): key(_key), val(_val), freq(_freq){}
    };
    class LFUCache {
        int minfreq, capacity;
        unordered_map<int, list<Node>::iterator> key_table;
        unordered_map<int, list<Node>> freq_table;
    public:
        LFUCache(int _capacity) {
            minfreq = 0;
            capacity = _capacity;
            key_table.clear();
            freq_table.clear();
        }
        
        int get(int key) {
            if (capacity == 0) return -1;
            auto it = key_table.find(key);
            if (it == key_table.end()) return -1;
            list<Node>::iterator node = it -> second;
            int val = node -> val, freq = node -> freq;
            freq_table[freq].erase(node);
            // 如果当前链表为空,我们需要在哈希表中删除,且更新minFreq
            if (freq_table[freq].size() == 0) {
                freq_table.erase(freq);
                if (minfreq == freq) minfreq += 1;
            }
            // 插入到 freq + 1 中
            freq_table[freq + 1].push_front(Node(key, val, freq + 1));
            key_table[key] = freq_table[freq + 1].begin();
            return val;
        }
        
        void put(int key, int value) {
            if (capacity == 0) return;
            auto it = key_table.find(key);
            if (it == key_table.end()) {
                // 缓存已满,需要进行删除操作
                if (key_table.size() == capacity) {
                    // 通过 minFreq 拿到 freq_table[minFreq] 链表的末尾节点
                    auto it2 = freq_table[minfreq].back();
                    key_table.erase(it2.key);
                    freq_table[minfreq].pop_back();
                    if (freq_table[minfreq].size() == 0) {
                        freq_table.erase(minfreq);
                    }
                } 
                freq_table[1].push_front(Node(key, value, 1));
                key_table[key] = freq_table[1].begin();
                minfreq = 1;
            } else {
                // 与 get 操作基本一致,除了需要更新缓存的值
                list<Node>::iterator node = it -> second;
                int freq = node -> freq;
                freq_table[freq].erase(node);
                if (freq_table[freq].size() == 0) {
                    freq_table.erase(freq);
                    if (minfreq == freq) minfreq += 1;
                }
                freq_table[freq + 1].push_front(Node(key, value, freq + 1));
                key_table[key] = freq_table[freq + 1].begin();
            }
        }
    };

    合并链表

     

    将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

    示例:

    输入:1->2->4, 1->3->4
    输出:1->1->2->3->4->4

     

    思路:链表归并。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
            ListNode head=new ListNode(0);
            ListNode temp=head;
            while(l1!=null && l2!=null){
                if(l1.val>l2.val){
                    temp.next=l2;
                    l2=l2.next;
                }else{
                    temp.next=l1;
                    l1=l1.next;  
                }
                temp=temp.next;
            }
            if(l1!=null){
                temp.next=l1;
            }else{
                temp.next=l2;
            }
            return head.next;
        }
    }

    反转链表

    反转一个单链表。

    示例:

    输入: 1->2->3->4->5->NULL
    输出: 5->4->3->2->1->NULL
     

    经典题不解释,画图自己模拟记得清楚

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode reverseList(ListNode head) {
            ListNode prev = null;
            ListNode curr = head;
            while (curr != null) {
                ListNode nextTemp = curr.next;
                curr.next = prev;
                prev = curr;
                curr = nextTemp;
            }
            return prev;
        }
    }

     反转链表2

    反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

    说明:
    1 ≤ m ≤ n ≤ 链表长度。

    示例:

    输入: 1->2->3->4->5->NULL, m = 2, n = 4
    输出: 1->4->3->2->5->NULL

    思路:反转链表,只不过是反转一部分,注意这一部分逆序之前做好记录,方便逆序完后可以链接上链表的其他部分。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode reverseBetween(ListNode head, int m, int n) {
            if (head == null) return null;
            ListNode cur = head, prev = null;
            while (m > 1) {
                prev = cur;
                cur = cur.next;
                m--;
                n--;
            }
            ListNode con = prev, tail = cur;
            ListNode third = null;
            while (n > 0) {
                third = cur.next;
                cur.next = prev;
                prev = cur;
                cur = third;
                n--;
            }
            if (con != null) {
                con.next = prev;
            } else {
                head = prev;
            }
            tail.next = cur;
            return head;
        }
    }

    对链表排序

    丢人,我就是按插入排序老老实实写的啊。。。。

    别人肯定map了hhh。

    对链表进行插入排序。


    插入排序的动画演示如上。从第一个元素开始,该链表可以被认为已经部分排序(用黑色表示)。
    每次迭代时,从输入数据中移除一个元素(用红色表示),并原地将其插入到已排好序的链表中。

     

    插入排序算法:

    插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
    每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
    重复直到所有输入数据插入完为止。
     

    示例 1:

    输入: 4->2->1->3
    输出: 1->2->3->4
    示例 2:

    输入: -1->5->3->4->0
    输出: -1->0->3->4->5

    思路:按插入排序思路写就可以啦,只是注意链表操作,比数组麻烦很多。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode insertionSortList(ListNode head) {
            ListNode ans=new ListNode(-1);
            ListNode temp=null;//要插入的地方
            ListNode key=null;//要插入的值
            while(head!=null){
                key=head;
                temp=ans;
                while(temp.next!=null && key.val>temp.next.val){
                    temp=temp.next;
                }
                head=head.next;
                key.next=temp.next;
                temp.next=key;
            }
            return ans.next;
    
        }
    }

    旋转链表

    给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

    示例 1:

    输入: 1->2->3->4->5->NULL, k = 2
    输出: 4->5->1->2->3->NULL
    解释:
    向右旋转 1 步: 5->1->2->3->4->NULL
    向右旋转 2 步: 4->5->1->2->3->NULL
    示例 2:

    输入: 0->1->2->NULL, k = 4
    输出: 2->0->1->NULL
    解释:
    向右旋转 1 步: 2->0->1->NULL
    向右旋转 2 步: 1->2->0->NULL
    向右旋转 3 步: 0->1->2->NULL
    向右旋转 4 步: 2->0->1->NULL

    思路:找准断点,直接调指针即可。

    注意:长度可能超过链表长度,要取模。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode rotateRight(ListNode head, int k) {
            if(head==null){
                return null;
            }
            int len=0;
            ListNode temp=head;
            while(temp!=null){
                temp=temp.next;
                len++;
            }
            k=k%len;
            ListNode node=head;
            ListNode fast=head;
            while(k-->0){
                fast=fast.next;
            }
            while(fast.next!=null){
                node=node.next;
                fast=fast.next;
            }
            fast.next=head;
            ListNode ans=node.next;
            node.next=null;
            return ans;
    
        }
    }

     数组实现栈

    学习了改进,利用define typedef比上次写的链表更容易改变功能,方便维护,代码更健壮。

    大佬别嫌弃,萌新总是很笨,用typedef都想不到。

    #include<stdio.h>
    #include<stdbool.h>
    #define maxsize 10
    typedef int datatype;
    typedef struct stack
    {
        datatype data[maxsize];
        int top;
    }Stack;
    Stack s;
    void init()//初始化
    {
        s.top=-1;
    }
    int Empty()//是否空
    {
        if(s.top==-1)return 1;
        return 0;
    }
    int full()//是否满
    {
        if(s.top==maxsize-1)return 1;
        return 0;
    }
    void Push(datatype element)//入栈
    {
        if(!full()){
            s.top++;
            s.data[s.top]=element;
        }
        else printf("栈满\n");
    }
    void Pop()//出栈
    {
        if(!Empty()) s.top--;
        else printf("栈空\n");
    }
    datatype Top()//取栈顶元素
    {
        if(!Empty()) return s.data[s.top];
        printf("栈空\n");
    }
    void Destroy()//销毁
    {
        s.top=-1;
    }

    测试不写了。

     

    链表实现栈

     

    栈,是操作受限的线性表,只能在一端进行插入删除。

    其实就是带尾指针的链表,尾插

    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    #define Status int
    #define SElemType int
    //只在头部进行插入和删除(不带头结点)
    typedef struct LNode
    {
    	SElemType data;
    	struct LNode *next;
    }LNode, *LinkList;
    
    typedef struct 
    {
    	LNode *top;
    	LNode *base;
    	int length;
    }LinkStack;
    
    
    Status InitStack(LinkStack &S)
    {
    	S.base = NULL;
    	S.top = NULL;
    	S.length = 0;
    	return OK;
    }
    
    Status GetTop(LinkStack S, SElemType &e)
    {
    	if(S.length == 0)
    		return ERROR;
    	e = S.top->data ;
    	return OK;
    }
    
    Status Push(LinkStack &S, SElemType e)
    {
    	LNode *newNode = (LNode *)malloc(sizeof(LNode));
    	newNode->data = e;
    	newNode->next = S.top;
    	S.top = newNode;
    	if(!S.base)
    		S.base = newNode;
    	++S.length;
    	return OK;
    }
    
    Status Pop(LinkStack &S, SElemType &e)
    {
    	LNode *p = S.top;
    	if(S.length == 0)
    		return ERROR;
    	e = S.top->data;
    	S.top = S.top->next;
    	free(p);
    	--S.length;
    	return OK;
    }
    
    void PrintStack(LinkStack S)
    {
    	LNode *p = S.top;
    	printf("由栈顶到栈底:");
    	while (p)
    	{
    		printf("%d  ",p->data);
    		p = p->next;
    	}
    	printf("\n");
    }
    
    
    int main(void)
    {
    	LinkStack LS;
    	InitStack(LS);
    	Push(LS,11);
    	Push(LS,22);
    	Push(LS,33);
    	Push(LS,44);
    	Push(LS,55);
    	PrintStack(LS);
    	SElemType e ;
    	GetTop(LS , e);
    	printf("栈顶元素是: %d\n",e);
    	Pop(LS,e);
    	PrintStack(LS);
    	Pop(LS,e);
    	PrintStack(LS);
    
    
    
    	return 0;
    }

    数组实现队列

     

    数组实现队列结构:

    相对栈结构要难搞一些,队列的先进先出的,需要一个数组和三个变量,size记录已经进来了多少个元素,不需要其它萌新看不懂的知识。

    触底反弹,头尾追逐的感觉。

    循环使用数组。

    具体解释一下触底反弹:当我们的队头已经到了数组的底,我们就把对头设为数组的第一个元素,对于队尾也是一样。实现了对数组的循环使用。

    #include<stdio.h>
    #include<stdbool.h>
    #define maxsize 10
    typedef int datatype;
    typedef struct queue
    {
        datatype arr[maxsize];
        int a,b,size;//头、尾、数量
    }queue;
    queue s;
    void init()//初始化
    {
        s.a=0;
        s.b=0;
        s.size=0;
    }
    int Empty()//判空
    {
        if(s.size==0)return 1;
        return 0;
    }
    int full()//判满
    {
        if(s.size==maxsize)return 1;
        return 0;
    }
    datatype peek()//查看队头
    {
        if(s.size!=0)return s.arr[s.a];
        printf("queue is null\n");
    }
    datatype poll()//弹出队头
    {
        int temp=s.a;
        if(s.size!=0)
        {
            s.size--;
            s.a=s.a==maxsize-1? 0 :s.a+1;//触底反弹
            return s.arr[temp];
        }
        printf("queue is null\n");
    }
    int push(datatype obj)//放入队尾
    {
        if(s.size!=maxsize)
        {
            s.size++;
            s.arr[s.b]=obj;
            s.b=s.b==maxsize-1? 0 : s.b+1;//触底反弹
            return 1;
        }
        printf("queue is full\n");
        return 0;
    }
    //测试
    int main()
    {
        int i;
        init();
        if(Empty())printf("null\n");
        for(i=0;i<20;i++)push(i);
        while(!Empty())
        {
            printf("%d\n",poll());
        }
        printf("%d",poll());
    }
    

    链表实现队列

     

    这次写的还算正规,稍微压缩了一下代码,但是不影响阅读

    画个图帮助理解:

    F->0->0->0<-R

    第一个0不存数据 

     

    #include<stdio.h>
    #include<malloc.h>
    #include<stdlib.h>
    typedef int Elementype;//数据类型
    //节点结构
    typedef struct Node{
        Elementype Element;//数据域
        struct Node * Next;
    }NODE,*PNODE;
    
    //    定义队列结构体
    typedef struct QNode {
        PNODE Front;//队头
        PNODE Rear;//队尾
    } Queue, *PQueue;
    
    void init(PQueue queue)//初始化
    {//头尾指向同一内存空间//头结点,不存数据
        queue->Front = queue->Rear = (PNODE)malloc(sizeof(NODE));
        queue->Front->Next = NULL;//头结点指针为空
    }
    
    int isEmpty(PQueue queue)//判空·
    {
        if(queue->Front == queue->Rear)return 1;
        return 0;
    }
    
    void insert(PQueue queue,Elementype data)//入队
    {
        PNODE P = (PNODE)malloc(sizeof(NODE));//初始化
        P->Element = data;
        P->Next = NULL;
        queue->Rear->Next = P;//入队
        queue->Rear = P;
    }
    
    void delete(PQueue queue,int * val)//出队,用val返回值
    {
        if(isEmpty(queue))printf("队空");
        else
        {
            PNODE  P = queue->Front->Next;//前一元素
            *val = P->Element;//记录值
            queue->Front->Next = P->Next;//出队
            //注意一定要加上判断,手动模拟一下就明白了
            if(P==queue->Rear)queue->Rear = queue->Front;
            free(P);//注意释放
            P = NULL;
        }
    }
    
    void destroy(PQueue queue)//释放
    {
        //从头开始删
        while(queue->Front != NULL)//起临时指针作用,无需再用别的空间
        {
            queue->Rear = queue->Front->Next;
            free(queue->Front);
            queue->Front = queue->Rear;
        }
    }
    //测试
    int main(void)
    {
        int i;
        int e;
        Queue a;
        PQueue queue=&a;
        init(queue);
        for(i=0;i<10;i++)
            insert(queue,i);
        while(!isEmpty(queue))//遍历
        {
            delete(queue,&e);
            printf("%d ",e);
        }
        if(isEmpty(queue))printf("1\n");
        delete(queue,&e);
        destroy(queue);
    }

    双栈的实现

    利用栈底位置相对不变的特性,可以让两个顺序栈共享一个空间。

    具体实现方法大概有两种:

    一种是奇偶栈,就是所有下标为奇数的是一个栈,偶数是另一个栈。但是这样一个栈的最大存储就确定了,并没有起到互补空缺的作用,我们实现了也就没有太大意义。

    还有一种就是,栈底分别设在数组的头和尾。进栈往中间进就可以了。这样,整个数组存满了才会真的栈满。

     

    那我们直接开始代码实现

     

    首先定义结构体:

    typedef struct
    {
      int top[2], bot[2];    //栈顶和栈底指针
      int *V;      //栈数组
      int m;     //栈最大可容纳元素个数
    }DblStack;

     

    初始化双栈s,长度为n:

    void Init(DblStack &S,int n)
    {
        S.m = n;
        S.V = new int [n+10];
        S.bot[0] = S.top[0] = -1;
        S.bot[1] = S.top[1] = S.m;  
    }

    判空:

    int EmptyStack0(DblStack S)
    {
        if(S.top[0]==-1)return 0;
        else return 1;
    }
    int EmptyStack1(DblStack S)
    {
        if(S.top[1]==S.m)return 0;
        else return 1;
    }

    判满:(没有单独判断一个栈的,是判断整个储存空间还有没有地方)

    int FullStack(DblStack S)
    {
        if(S.top[1]-S.top[0]==1)return 1;
        else return 0;
    }

    进栈:

    void Push0(DblStack &S,int e)
    {
        if(S.top[1]-S.top[0]!=1)
        {
            S.top[0]++;
            S.V[S.top[0]] = e;
        }
    }
    
    void Push1(DblStack &S,int e)
    {
        if(S.top[1]-S.top[0] != 1)
        {
            S.top[1]--;
            S.V[S.top[1]] = e;
        }
    }

    出栈:

    void Pop0(DblStack &S,int &e)
    {
        if(S.top[0]!=-1)
        {
            e = S.V[S.top[0]];
            S.top[0]--;
        }
    }
    
    void Pop1(DblStack &S,int &e)
    {
        if(S.top[1]!=S.m)
        {
            e = S.V[S.top[1]];
            S.top[1]++;
        }
    }

     栈/队列 互相模拟实现

     

    用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

    思路:大概这么想:用一个辅助栈把进第一个栈的元素倒一下就好了。

    比如进栈1,2,3,4,5

    第一个栈:

    5

    4

    3

    2

    1

    然后倒到第二个栈里

    1

    2

    3

    4

    5

    再倒出来,顺序为1,2,3,4,5

    实现队列

    然后要注意的事情:

    1)栈2非空不能往里面倒数,顺序就错了。栈2没数再从栈1倒。

    2)栈1要倒就一次倒完,不倒完的话,进新数也会循序不对。

    import java.util.Stack;
     
    public class Solution {
        Stack<Integer> stack1 = new Stack<Integer>();
        Stack<Integer> stack2 = new Stack<Integer>();
         
        public void push(int node) {
            stack1.push(node);
        }
         
        public int pop() {
            if(stack1.empty()&&stack2.empty()){
                throw new RuntimeException("Queue is empty!");
            }
            if(stack2.empty()){
                while(!stack1.empty()){
                    stack2.push(stack1.pop());
                }
            }
            return stack2.pop();
        }
    }

     

    用两个队列实现栈,要求同上:

    这其实意义不是很大,有些数据结构书上甚至说两个队列不能实现栈。

    其实是可以的,只是时间复杂度较高,一个弹出操作时间为O(N)。

    思路:两个队列,编号为1和2.

    进栈操作:进1号队列

    出栈操作:把1号队列全弄到2号队列里,剩最后一个别压入,而是返回。

    最后还得把1和2号换一下,因为现在是2号有数,1号空。

     

    仅仅有思考价值,不实用。

    比如压入1,2,3

    队列1:1,2,3

    队列2:空

    依次弹出1,2,3:

    队列1里的23进入2号,3弹出

    队列1:空

    队列2:2,3

     

    队列2中3压入1号,2弹出

    队列1:3

    队列2:空

     

    队列1中只有一个元素,弹出。

     

    上代码:

    public class TwoQueueImplStack {
    	Queue<Integer> queue1 = new ArrayDeque<Integer>();
    	Queue<Integer> queue2 = new ArrayDeque<Integer>();
    //压入
    	public void push(Integer element){
    		//都为空,优先1
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    		//1为空,2有数据,放入2
    		if(queue1.isEmpty()){
    			queue2.add(element);
    			return;
    		}
    		//2为空,1有数据,放入1
    		if(queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    	}
    //弹出
    	public Integer pop(){
    		//两个都空,异常
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			try{
    				throw new Exception("satck is empty!");
    			}catch(Exception e){
    				e.printStackTrace();
    			}
    		}	
    		//1空,2有数据,将2中的数据依次放入1,最后一个元素弹出
    		if(queue1.isEmpty()){
    			while(queue2.size() > 1){
    				queue1.add(queue2.poll());
    			}
    			return queue2.poll();
    		}
    		
    		//2空,1有数据,将1中的数据依次放入2,最后一个元素弹出
    		if(queue2.isEmpty()){
    			while(queue1.size() > 1){
    				queue2.add(queue1.poll());
    			}
    			return queue1.poll();
    		}
    		
    		return (Integer)null;
    	}
    //测试
    	public static void main(String[] args) {
    		TwoQueueImplStack qs = new TwoQueueImplStack();
    		qs.push(2);
    		qs.push(4);
    		qs.push(7);
    		qs.push(5);
    		System.out.println(qs.pop());
    		System.out.println(qs.pop());
    		
    		qs.push(1);
    		System.out.println(qs.pop());
    	}
    }
    

     

    栈的排序

      一个栈中元素的类型为整型,现在想将该栈从顶到底按从大到小的顺序排序,只许申请一个栈。除此之外,可以申请新的变量,但是不能申请额外的数据结构,如何完成排序?

    思路:

        将要排序的栈记为stack,申请的辅助栈记为help.在stack上执行pop操作,弹出的元素记为cru.

          如果cru小于或等于help的栈顶元素,则将cru直接压入help.

          如果cru大于help的栈顶元素,则将help的元素逐一弹出,逐一压入stack,直到cru小于或等于help的栈顶元素,再将cru压入help.

    一直执行以上操作,直到stack中的全部元素压入到help,最后将heip中的所有元素逐一压入stack,完成排序。

     

    其实和维持单调栈的思路挺像的,只是弹出后没有丢弃,接着放。

    和基础排序也挺像。

     

    import java.util.Stack;
    public class a{
       public static void sortStackByStack(Stack<Integer> stack){
           Stack<Integer> help=new Stack<Integer>();
           while(!stack.isEmpty()){
               int cru=stack.pop();
               while(!help.isEmpty()&&help.peek()<cru){
                   stack.push(help.pop());
               }
               help.push(cru);
           }
           while (!help.isEmpty()) {
                 stack.push(help.pop());        
        }
       }
    }

    栈——括号匹配

    栈的应用,括号匹配。

    经典做法是,遇左括号压入,遇右括号判断,和栈顶配对就继续,不配对或者栈空就错了。最后判断是否为空。

    代码有些麻烦。

     

    我是遇左括号压对应的右括号,最后判断代码就会很简单:相等即可。

    class Solution {
    public:
        bool isValid(string s) {
            int len=s.size();
            stack<char> st;
            for(int i=0;i<len;i++){
                if(s[i]=='(')st.push(')');
                else if(s[i]=='[')st.push(']');
                else if(s[i]=='{')st.push('}');
                else if(st.empty())return false;
                else if(st.top()!=s[i])return false;
                else st.pop();
            }
            return st.empty();
        }
    };

    栈——表达式求值 

    今天把表达式求值给搞定吧。

     

    问题:给你个表达式,有加减乘除和小括号,让算出结果。

    我们假定计算式是正确的,并且不会出现除数为0等错误。

    py大法好啊,在保证可读性的前提下能压到一共就三十多行代码。

    其实能压到不到三十行,但是代码就不好看了。。。。

    计算函数:

    def getvalue(a, b, op):
        if op == "+":return a+b
        elif op == "-":return a-b
        elif op == "*":return a*b
        else:return a/b

     

    出栈一个运算符,两个数值,计算,将结果入data用于之后计算

    def process(data, opt):
        operator = opt.pop()
        num2 = data.pop()
        num1 = data.pop()
        data.append(getvalue(num1, num2, operator))

    比较符号优先级:
    乘除运算优先级比加减高。

    op1优先级比op2高返回True,否则返回False

    def compare(op1, op2):
        return op1 in ["*","/"] and op2 in ["+","-"]

    主函数:

    基本思路:

    处理每个数字为一个整数,处理每一项为一个单独的数字,把括号内处理为一个单独的数字。

    把式子处理为只有整数、加减的式子再最后计算。

    def calculate(s):
        data = []#数据栈
        opt = []#操作符栈
        i = 0  #表达式遍历的索引
        while i < len(s):
            if s[i].isdigit():  # 数字,入栈data
                start = i
                while i+1  < len(s) and s[i + 1].isdigit():
                    i += 1
                data.append(int(s[start: i + 1]))  # i为最后一个数字字符的位置
            elif s[i] == ")":  # 右括号,opt出栈,data出栈并计算,结果入data,直到左括号
                while opt[-1] != "(":
                    process(data,opt)#优先级高的一定先弹出
                opt.pop()  # 出栈的一定是左括号
            elif not opt or opt[-1] == "(":opt.append(s[i])#栈空,或栈顶为左括号,入opt
            elif s[i]=="(" or compare(s[i],opt[-1]):opt.append(s[i])#左括号或比栈顶优先级高,入
            else: #优先级不比栈顶高,opt出栈同时data出栈并计算,计算结果入data
                while opt and not compare(s[i], opt[-1]):
                    if opt[-1] == "(":break  #遇到左括号,停止计算
                    process(data, opt)
                opt.append(s[i])
            i += 1  #索引后移
        while opt:
            process(data, opt)
        print(data.pop())

    借汉诺塔理解栈与递归

    我们先说,在一个函数中,调用另一个函数。

    首先,要意识到,函数中的代码和平常所写代码一样,也都是要执行完的,只有执行完代码,或者遇到return,才会停止。

    那么,我们在函数中调用函数,执行完了,就会重新回到本函数中,继续向下执行,直到结束。

    在执行其它函数时,本函数相当于中断了,不执行了。那我们重新回来的时候,要从刚才暂停的地方开始,继续执行,这期间,所有现场信息都要原封不动,就相当于时间暂停了一样,什么都不能改变,这样才能做到程序的准确。

    所以,通常,在执行另一个函数之前,电脑会将现场信息压入一个系统栈,为被调用的函数分配存储区,然后开始执行被调函数。执行完毕后,保存计算结果,释放被调函数的空间,按照被调函数里保存的返回地址,返回到原函数。

    那什么是递归函数呢?

    就是多个函数嵌套调用。不同的是,这些函数是同一个函数,只是参数可能不同,甚至参数也一样,只是存储空间不同。

    每一层递归所需信息构成一个栈,每一块内存储着所有实在参数,所有局部变量,上一层的返回地址,等等一切现场信息。每执行完就弹出。

    递归函数有着广泛应用,主要适合可以把自身分化成一样的子问题的问题。比如汉诺塔。

     

    汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

    思路:函数(n,a,b,c)含义是把n个盘子从a柱子搬到c柱子的方法

    一个盘子,直接搬过去。

    多个盘子,我们把n-1个盘子都移动到另一个柱子上,把最大的搬过去然后把剩下的搬过去。

     

    def hanoi(n, a, b, c):
        if n == 1:
            print(a, '-->', c)
        else:
            hanoi(n - 1, a, c, b)
            print(a, '-->', c)
            hanoi(n - 1, b, a, c)
    # 调用
    hanoi(3, 'A', 'B', 'C')

    结果:

    A --> C
    A --> B
    C --> B
    A --> C
    B --> A
    B --> C
    A --> C

    我们的栈:

    第一次:

    我们把hanoi(3, 'A', 'B', 'C')存了起来,调用了hanoi(3-1, 'A', 'C', 'B'),现在栈里压入了3, 'A', 'B', 'C',还有函数执行到的位置等现场信息。然后执行hanoi(3-1, 'A', 'C', 'B'),发现要调用hanoi(3-1-1, 'A', 'B', 'C'),我们又把3-1, 'A', 'C', 'B'等信息压入了栈,现在栈是这样的:

    栈头

    2, 'A', 'C', 'B'

    3, 'A', 'B', 'C'

    栈尾

     

    然后执行hanoi(3-1-1, 'A', 'B', 'C'),发现n=1了,打印了第一条A --> C,然后释放掉了hanoi(3-1-1, 'A', 'B', 'C')的空间,并通过记录的返址回到了hanoi(3-1, 'A', 'C', 'B'),然后执行打印语句A --> B,然后发现要调用hanoi(3-1-1, 'C', 'A', 'B'),此时栈又成了:

    2, 'A', 'C', 'B'
    3, 'A', 'B', 'C'

    调用hanoi(1, 'A', 'C', 'B')发现可以直接打印,C --> B。

    然后我们又回到了2, 'A', 'C', 'B'这里。发现整个函数执行完了,那就弹出吧。这时栈是这样的:

    3, 'A', 'B', 'C'

    只有这一个。

    我们继续执行这个函数的代码,发现

    def hanoi(n, a, b, c):
        if n == 1:
            print(a, '-->', c)
        else:
            hanoi(n - 1, a, c, b)//执行到了这里
            print(a, '-->', c)
            hanoi(n - 1, b, a, c)

     

    那我们就继续执行,发现要打印A --> C

    然后继续,发现要调用        hanoi(n - 1, b, a, c),那我们继续把现场信息压栈,继续执行就好了。

     

    递归就是把大问题分解成小问题进而求解。

    具体执行就是通过系统的栈来实现返回原函数的功能。

     转存失败重新上传取消 

     

    多色汉诺塔问题:

     

    奇数号圆盘着蓝色,偶数号圆盘着红色,如图所示。现要求将塔座A 上的这一叠圆盘移到塔座B 上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:

    规则(1):每次只能移动1 个圆盘;
    规则(2):任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
    规则(3):任何时刻都不允许将同色圆盘叠在一起;
     

    其实双色的汉诺塔就是和无色的汉诺塔算法类似,通过推理可知,无色汉诺塔的移动规则在双色汉诺塔这里的移动规则并没有违反。

    这里说明第一种就可以了:Hanoi(n-1,A,C,B);

    在移动过程中,塔座上的最低圆盘的编号与n-1具有相同奇偶性,塔座b上的最低圆盘的编号与n-1具有不相同的奇偶性,从而塔座上的最低圆盘的编号与n具有相同的奇偶性,塔座上c最低圆盘的编号与n具有不同的奇偶性;
     

    所以把打印操作换成两个打印即可

     

    总:因为递归可能会有重复子问题的出现。

    就算写的很好,无重复子问题,也会因为来回调用、返回函数,而速度较慢。所以,有能力的可以改为迭代或动态规划等方法。

     

    单调栈

    通过使用栈这个简单的结构,我们可以巧妙地降低一些问题的时间复杂度。

    单调栈性质:

    1、若是单调递增栈,则从栈顶到栈底的元素是严格递增的。若是单调递减栈,则从栈顶到栈底的元素是严格递减的。

    2、越靠近栈顶的元素越后进栈。(显而易见)

    本文介绍单调栈用法

    通过一道题来说明。

    POJ2559

    1. 题目大意:链接

    给出一个柱形统计图(histogram), 它的每个项目的宽度是1, 高度和具体问题有关。 现在编程求出在这个柱形图中的最大面积的长方形。

    7 2 1 4 5 1 3 3

    7表示柱形图有7个数据,分别是 2 1 4 5 1 3 3, 对应的柱形图如下,最后求出来的面积最大的图如右图所示。

    做法1:枚举每个起点和终点,矩形面积就是长*最小高度。O(N^3)

    做法2:区间最小值优化。O(N^2)

    做法3:以每一个下标为中心向两边扩,遇到更短的就停,这样我们可以确定以每一个下标高度为最高的矩形。O(N^2)

    单调栈:维护一个单调递增栈,所有元素各进栈和出栈一次即可。每个元素出栈的时候更新最大的矩形面积。

    过程:

    1)判断当前元素小于栈顶

    2)条件满足,就可以更新栈顶元素的最大长度了,并且把栈顶弹出

    3)继续执行(1),直到条件不满足。

     

    重要结论:

    1)栈顶下面一个元素一定是,栈顶左边第一个比栈顶小的元素

    2)当前元素一定是,右边第一个比栈顶小的元素。

    为什么呢?

    比如这是个栈

    1)如果右边存在距离更近的比1号小的数,1号早已经弹出了。

    2)如果左边有距离更近的比1号小的数

                    如果它比2号小,它会把2号弹出,自己成为2号

                     如果它比2号大,它不会弹出2号,但是它会压栈,变成2号,原来的2号成为3号。

    所以不管怎么说,这个逻辑是正确的。

    最后放代码并讲解

     

    下面看一道难一些的题

    LeetCode 85 Maximal Rectangle

    1 0 1 0 0

    1 0 1 1 1

    1 1 1 1 1

    1 0 0 1 0

    Return 6.二三行后面那六个1

     

    给定一个由二进制组成的矩阵map,找到仅仅包含1的最大矩形,并返回其面积。

    这道题是一行一行的做。对每一行都求出每个元素对应的高度,这个高度就是对应的连续1的长度,然后对每一行都更新一次最大矩形面积。

    连续1长度也很好更新,本个元素是0,长度就是0,本个元素是1,那就加上之前的。

    具体思路代码中讲解。

    import java.util.Stack;
    
    public class MaximalRectangle {
    
    	public static int maxRecSize(int[][] map) {
    		if (map == null || map.length == 0 || map[0].length == 0) {
    			return 0;
    		}
    		int maxArea = 0;
    		int[] height = new int[map[0].length];
    		for (int i = 0; i < map.length; i++) {
    			for (int j = 0; j < map[0].length; j++) {
    				height[j] = map[i][j] == 0 ? 0 : height[j] + 1;//0长度为0,1长度为前面+1
    			}
    			maxArea = Math.max(maxRecFromBottom(height), maxArea);//调用第一题的思想
    		}
    		return maxArea;
    	}
    
    	//第一题思路
    	public static int maxRecFromBottom(int[] height) {
    		if (height == null || height.length == 0) {
    			return 0;
    		}
    		int maxArea = 0;
    		Stack<Integer> stack = new Stack<Integer>();
    		for (int i = 0; i < height.length; i++) {
                    //栈非空并且栈顶大
    			while (!stack.isEmpty() && height[i] <= height[stack.peek()]) {
    				int j = stack.pop();//弹出
    				int k = stack.isEmpty() ? -1 : stack.peek();
    				int curArea = (i - k - 1) * height[j];//计算最大
    				maxArea = Math.max(maxArea, curArea);//更新总体最大
    			}
    			stack.push(i);//直到栈顶小,压入新元素
    		}
    		//最后栈非空,右边没有更小元素使它们弹出
    		while (!stack.isEmpty()) {
    			int j = stack.pop();
    			int k = stack.isEmpty() ? -1 : stack.peek();
    			int curArea = (height.length - k - 1) * height[j];
    			maxArea = Math.max(maxArea, curArea);
    		}
    		return maxArea;
    	}
    
    	public static void main(String[] args) {
    		int[][] map = { { 1, 0, 1, 1 }, { 1, 1, 1, 1 }, { 1, 1, 1, 0 }, };
    		System.out.println(maxRecSize(map));
    	}
    
    }

     

    双端单调队列

     

    这次介绍一种新的数据结构:双端队列:双端队列是指允许两端都可以进行入队和出队操作的队列,其元素的逻辑结构仍是线性结构。将队列的两端分别称为前端和后端,两端都可以入队和出队。

    堆栈、队列和优先队列都可以采用双端队列来实现

    本文介绍单调双端队列的原理及应用。

    单调队列,顾名思义,就是一个元素单调的队列,那么就能保证队首的元素是最小(最大)的,从而满足最优性问题的需求。

    给定一个长度为n的数列,一个k,求所有的min(ai,ai+1.....ai+k-1),i=0,1,....n-k

    通俗一点说就是一个长度固定的滑动的窗口,求每个窗口内的最小值。

    你当然可以暴力求解,依次遍历每个窗口.

    介绍单调队列用法:我们维护一个单调队列

    单调队列呢,以单调递增序列为例:

    1、如果队列的长度一定,先判断队首元素是否在规定范围内,如果超范围则增长队首。

    2、每次加入元素时和队尾比较,如果当前元素小于队尾且队列非空,则减小尾指针,队尾元素依次出队,直到满足队列的调性为止

     

    我们说算法的优化就是重复计算过程的去除。

    按窗口一次次遍历就是重复计算。最值信息没有利用好。

    我们为什么可以这么维护?

    首先,遍历到的元素肯定在队列元素之后。

    其次,如果当前元素更小的话。

    头部的值比当前元素大,头部还比当前元素先过期。所以以后计算再也不会用到它了。我们可以放心的去掉它。

    下面给出代码和解释

    int n,k;//长度为n的数列,窗口为k
    int a[MAX_N];//数列
    int b[MAX_N];//存放
    int deq[MAX_N]//模拟队列
    
    void solve()
    {
        int s = 0,t = 0;//头和尾
        for(int i=0;i<n;i++)
        {
            //不满足单调,尾就弹出
            while(s<t && a[deq[t-1]]>=a[i])t--;
            //直到满足,放入
            deq[t++]=i;
            //计算窗口最大值
            if(i-k+1>=0)b[i-k+1]=a[deq[s];
            //判断头过期弹出
            if(deq[s]==i-k+1)s++;
        }
    }

    基本入门就到这里。

     单调队列优化的背包问题

    对于背包问题,经典的背包九讲已经讲的很明白了,本来就不打算写这方面问题了。

    但是吧。

    我发现,那个最出名的九讲竟然没写队列优化的背包。。。。

    那我必须写一下咯嘿嘿,这么好的思想。

     

    我们回顾一下背包问题吧。

     

    01背包问题 


    题目 
    有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

    这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 

    f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:

    f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。 

    就是说,对于本物品,我们选择拿或不拿

    比如费用是3.

    相关图解:

    我们求表格中黄色部分,只和两个黑色部分有关

    拿了,背包容量减少,我们价值加上减少后最大价值。

    不拿,最大价值等于没有这件物品,背包不变,的最大价值。

    完全背包问题 


    题目 
    有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 


    基本思路 
    这个问题非常类似于01背包问题,所不同的是每种物品有无限件。

    f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}

    图解:

    因为我们拿了本物品还可以继续拿无限件,对于当前物品,无论之前拿没拿,还可以继续拿,所以是f[i][v-c[i]]+w[i]

     

    换一个角度说明这个问题为什么可以f[i][v-c[i]]+w[i],也就是同一排。

    其实是这样的,我们对于黄色部分,也就是当前物品,有很多种选择,可以拿一个,两个。。。一直到背包容量不够了。

    也就是说,可以不拿,也就是J1,可以拿一个,也就是G1+w[i],也可以拿两个,也就是D1+2w[i],拿三个,A1+3w[i]。

    但是我们看G2,G2其实已经是之前的最大了:A1+2w[i],D1+w[i],G1他们中最大的,对么?

    既然G2是他们中最大的。

    我们怎么求J2?

    是不是只要求G2+w[i]和J1的最大值就好了。

    因为G2把剩下的情况都保存好了。

     

    多重背包问题 


    题目 
    有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

     

    和之前的完全背包不同,这次,每件物品有最多拿n[i]件的限制。

    思路一:我们可以把物品全都看成01背包:比如第i件,我们把它拆成n[i]件一样的单独物品即可。

    思路二:思路一时间复杂度太高。利用二进制思路:一个n位二进制,能表示2^n种状态,如果这些状态就是拿了多少物品,我们可以把每一位代表的数都拿出来,比如n[i]=16,我们把它拆成1,2,4,8,1,每一堆物品看成一个单独物品。

    为什么最后有个一?因为从0到16有十七种状态,四位不足以表示。我们最后补上第五位1.

    把拆出来的物品按01背包做即可。

    思路三:我们可以利用单调队列:

    https://blog.csdn.net/hebtu666/article/details/82720880

    再回想完全背包:为什么可以那么做?因为每件物品能拿无限件。所以可以。而多重背包因为有了最多拿多少的限制,我们就不敢直接从G2中拿数,因为G2可能是拿满了本物品以后才达到的状态 。

    比如n[i]=2,如果G2的状态是2w[i],拿了两个2物品达到最大值,我们的J2就不能再拿本物品了。

    如何解决这个问题?就是我给的网址中的,双端单调队列

    利用窗口最大值的思想。

    大家想想怎么实现再看下文。

     

    发现问题了吗?

    我们求出J2以后,按原来的做法,是该求K2的,但是K2所需要的信息和J2完全不同,红色才是K2可能需要的信息。

    所以我们以物品重量为差,先把黑色系列推出来,再推红色系列,依此类推。

    这个例子就是推三次,每组各元素之间差3.

    这样就不会出现构造一堆单调队列的尴尬情况了。

    在代码中继续详细解释:

    //输入
    int n;
    int W;
    int w[MAX_N];
    int v[MAX_N];
    int m[MAX_N];

     

    int dp[MAX_N+1];//压空间,本知识参考https://blog.csdn.net/hebtu666/article/details/79964233
    int deq[MAX_N+1];//双端队列,保存下标
    int deqv[MAX_N+1];//双端队列,保存值

    队列存的就是所有上一行能取到的范围,比如对于J2,队列里存的就是G1-w[i],D1-2w[i],A1-3w[i]等等合法情况。(为了操作方便都是j,利用差实现最终的运算)

    他们之中最大的就是队头,加上最多存储个数就好。

     

     

     

    void solve()
    {
        for(int i=0;i<n;i++)//参考过那个网址第二题应该懂
        {
            for(int a=0;a<w[i];a++)//把每个分组都打一遍
            {
                int s=0;//初始化双端队列头尾
                int t=0;
                for(int j=0;j*w[i]+a<=W;j++)//每组第j个元素
                {
                    int val=dp[j*w[i]+a]-j*v[i];
                    while(s<t && deqv[t-1]<=val)//直到不改变单调性
                        t--;
                    deq[t]=j;
                    deqv[t]=val;
                    t++;
                    //利用队头求出dp
                    dp[j*w[i]+a]=deqv[s]+j*v[i];
                    if(deq[s]==j-m[i])s++;//检查过期
                }
            }
        }
    }

     串的定长表示

    思想和代码都不难,和线性表也差不多,串本来就是数据受限的线性表。

    串连接:

     

    #include <stdio.h>
    #include <string.h>
    //串的定长顺序存储表示
    #define MAXSTRLEN 255							//用户可在255以内定义最大串长
    typedef unsigned char SString[MAXSTRLEN + 1];	//0号单元存放串的长度
    
    int Concat(SString *T,SString S1,SString S2)
    	//用T返回S1和S2联接而成的新串。若未截断返回1,若截断返回0
    {
    	int i = 1,j,uncut = 0;
    	if(S1[0] + S2[0] <= MAXSTRLEN)	//未截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    		for (j = 1; j <= S2[0]; j++)
    			(*T)[S1[0]+j] = S2[j];	//(*T)[i+j] = S2[j]
    		(*T)[0] = S1[0] + S2[0];
    		uncut = 1;
    	}
    	else if(S1[0] < MAXSTRLEN)		//截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    
    		for (j = S1[0] + 1; j <= MAXSTRLEN; j++)
    		{
    			(*T)[j] = S2[j - S1[0] ];
    			(*T)[0] = MAXSTRLEN;
    			uncut = 0;
    		}
    	}
    	else
    	{
    		for (i = 0; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		/*或者分开赋值,先赋值内容,再赋值长度
    		for (i = 1; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		(*T)[0] = MAXSTRLEN;
    		*/
    		uncut = 0;
    	}
    
    	return uncut;
    }
    
    int SubString(SString *Sub,SString S,int pos,int len)
    	//用Sub返回串S的第pos个字符起长度为len的子串
    	//其中,1 ≤ pos ≤ StrLength(S)且0 ≤ len ≤ StrLength(S) - pos + 1(从pos开始到最后有多少字符)
    	//第1个字符的下标为1,因为第0个字符存放字符长度
    {
    	int i;
    	if(pos < 1 || pos > S[0] || len < 0 || len > S[0] - pos + 1)
    		return 0;
    	for (i = 1; i <= len; i++)
    	{
    		//S中的[pos,len]的元素 -> *Sub中的[1,len]
    		(*Sub)[i] = S[pos + i - 1];//下标运算符 > 寻址运算符的优先级
    	}
    	(*Sub)[0] = len;
    	return 1;
    }
    void PrintStr(SString S)
    {
    	int i;
    	for (i = 1; i <= S[0]; i++)
    		printf("%c",S[i]);
    	printf("\n");
    }
    
    
    int main(void)
    {
    	/*定长顺序存储初始化和打印的方法
    	SString s = {4,'a','b','c','d'};
    	int i;
    	//s = "abc";	//不可直接赋值
    	for (i = 1; i <= s[0]; i++)
    		printf("%c",s[i]);
    	*/
    	SString s1 = {4,'a','b','c','d'};
    	SString s2 = {4,'e','f','g','h'},s3;
    	SString T,Sub;
    	int i;
    	
    	for (i = 1; i <= 255; i++)
    	{
    		s3[i] = 'a';
    		if(i >= 248)
    			s3[i] = 'K';
    	}
    	s3[0] = 255;
    	SubString(&Sub,s3,247,8);
    	PrintStr(Sub);
    	
    
    
    
    	return 0;
    }

    串的堆分配实现

     

    今天,线性结构基本就这样了,以后(至少是最近)就很少写线性基础结构的实现了。

    串的类型定义

    typedef struct
    {
        char *str;
        int length;
    }HeapString;


    初始化串

    InitString(HeapString *S)
    {
        S->length=0;
        S->str='\0';
    }

    长度

    int StrEmpty(HeapString S)
    /*判断串是否为空,串为空返回1,否则返回0*/
    {
        if(S.length==0)         /*判断串的长度是否等于0*/
            return 1;           /*当串为空时,返回1;否则返回0*/
        else
            return 0;
    }
    int StrLength(HeapString S)
    /*求串的长度操作*/
    {
        return S.length;
    }


    串的赋值

    void StrAssign(HeapString *S,char cstr[])
    /*串的赋值操作*/
    {
        int i=0,len;
        if(S->str)
            free(S->str);
        for(i=0;cstr[i]!='\0';i++); /*求cstr字符串的长度*/
            len=i;
        if(!i)
        {
            S->str=NULL;
            S->length=0;
        }
        else
        {
            S->str=(char*)malloc((len+1)*sizeof(char));
            if(!S->str)
                exit(-1);
            for(i=0;i<len;i++)
                S->str[i]=cstr[i];
    
            S->length=len;
        }
    }


    串的复制

    void StrAssign(HeapString *S,char cstr[])
    /*串的赋值操作*/
    {
        int i=0,len;
        if(S->str)
            free(S->str);
        for(i=0;cstr[i]!='\0';i++); /*求cstr字符串的长度*/
            len=i;
        if(!i)
        {
            S->str=NULL;
            S->length=0;
        }
        else
        {
            S->str=(char*)malloc((len+1)*sizeof(char));
            if(!S->str)
                exit(-1);
            for(i=0;i<len;i++)
                S->str[i]=cstr[i];
    
            S->length=len;
        }
    }


    串的插入

    int StrInsert(HeapString *S,int pos,HeapString T)
    /*串的插入操作。在S中第pos个位置插入T分为三种情况*/
    {
        int i;
        if(pos<0||pos-1>S->length)      /*插入位置不正确,返回0*/
        {
            printf("插入位置不正确");
            return 0;
        }
        S->str=(char*)realloc(S->str,(S->length+T.length)*sizeof(char));
        if(!S->str)
        {
            printf("内存分配失败");
            exit(-1);
        }
    
        for(i=S->length-1;i>=pos-1;i--)
            S->str[i+T.length]=S->str[i];
        for(i=0;i<T.length;i++)
            S->str[pos+i-1]=T.str[i];
    
        S->length=S->length+T.length;
        return 1;
    }


    串的删除

    int StrDelete(HeapString *S,int pos,int len)
    /*在串S中删除pos开始的len个字符*/
    {
        int i;
        char *p;
        if(pos<0||len<0||pos+len-1>S->length)
        {
            printf("删除位置不正确,参数len不合法");
            return 0;
        }
        p=(char*)malloc(S->length-len);             /*p指向动态分配的内存单元*/
        if(!p)
            exit(-1);
        for(i=0;i<pos-1;i++)                        /*将串第pos位置之前的字符复制到p中*/
            p[i]=S->str[i];
        for(i=pos-1;i<S->length-len;i++)                /*将串第pos+len位置以后的字符复制到p中*/
            p[i]=S->str[i+len];
        S->length=S->length-len;                    /*修改串的长度*/
        free(S->str);                           /*释放原来的串S的内存空间*/
        S->str=p;                               /*将串的str指向p字符串*/
        return 1;
    }



    串的比较

    int StrCompare(HeapString S,HeapString T)
    /*串的比较操作*/
    {
    int i;
    for(i=0;i<S.length&&i<T.length;i++) /*比较两个串中的字符*/
        if(S.str[i]!=T.str[i])          /*如果出现字符不同,则返回两个字符的差值*/
            return (S.str[i]-T.str[i]);
    return (S.length-T.length);             /*如果比较完毕,返回两个串的长度的差值*/
    }


    串的连接

    int StrCat(HeapString *T,HeapString S)
    /*将串S连接在串T的后面*/
    {
        int i;
        T->str=(char*)realloc(T->str,(T->length+S.length)*sizeof(char));
        if(!T->str)
        {
            printf("分配空间失败");
            exit(-1);
        }
        else
        {
            for(i=T->length;i<T->length+S.length;i++)   /*串S直接连接在T的末尾*/
                T->str[i]=S.str[i-T->length];
            T->length=T->length+S.length;           /*修改串T的长度*/
        }
        return 1;
    }


    清空串

    void StrClear(HeapString *S)
    /*清空串,只需要将串的长度置为0即可*/
    {
    
        S->str='\0';
        S->length=0;
    }


    销毁串

    void StrDestroy(HeapString *S)
    {
        if(S->str)
            free(S->str);
    }

    打印

    void StrPrint(HeapString S)
    {
        int i;
        for(i=0;i<S.length;i++)
        {
            printf("%c",S.str[i]);
        }
        printf("\n");
    }

    KMP

    Kmp操作、原理、拓展

     

     

    注:虽然我是一只菜,才大一。但我是想让萌新们更容易的学会一些算法和思想,所以没有什么专业词语,用的都是比较直白地表达,大佬们可能觉得烦,但是真的对不会的人更有帮助啊。我本人也是菜,大一上学期写的,直接拿过来了,也没检查,有什么错误大佬们赶紧告诉我

    先上代码,大佬们可以别看下面了,就当复习一下

    package advanced_001;
    
    public class Code_KMP {
    
    	public static int getIndexOf(String s, String m) {
    		if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
    			return -1;
    		}
    		char[] str1 = s.toCharArray();
    		char[] str2 = m.toCharArray();
    		int i1 = 0;
    		int i2 = 0;
    		int[] next = getNextArray(str2);
    		while (i1 < str1.length && i2 < str2.length) {
    			if (str1[i1] == str2[i2]) {
    				i1++;
    				i2++;
    			} else if (next[i2] == -1) {
    				i1++;
    			} else {
    				i2 = next[i2];
    			}
    		}
    		return i2 == str2.length ? i1 - i2 : -1;
    	}
    
    	public static int[] getNextArray(char[] ms) {
    		if (ms.length == 1) {
    			return new int[] { -1 };
    		}
    		int[] next = new int[ms.length];
    		next[0] = -1;
    		next[1] = 0;
    		int i = 2;
    		int cn = 0;
    		while (i < next.length) {
    			if (ms[i - 1] == ms[cn]) {
    				next[i++] = ++cn;
    			} else if (cn > 0) {
    				cn = next[cn];
    			} else {
    				next[i++] = 0;
    			}
    		}
    		return next;
    	}
    
    	public static void main(String[] args) {
    		String str = "abcabcababaccc";
    		String match = "ababa";
    		System.out.println(getIndexOf(str, match));
    
    	}
    
    }
    

     

    问题:给定主串S和子串 T,如果在主串S中能够找到子串 T,则匹配成功,返回第一个和子串 T 中第一个字符相等的字符在主串S中的序号;否则,称匹配失败,返回 0。

     

    一、引子

    原始算法:以主串中每一个位置为开头,与子串第一个元素匹配,若相同,下一个位置和子串下一个位置匹配,如果子串元素全部匹配成功,则匹配成功,找到位置。

    非常傻白甜,很明显时间复杂度最差为o(len(s)*len(t))。效率很低,大佬请忽略:

     

    引出KMP算法,概念如下:KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。(摘自百度百科)

     

     

    其实就是说,人家kmp算法时间复杂度o(len(s)+len(t)),非常快了,毕竟你不遍历一遍这俩字符串,怎么可能匹配出来呢?我不信还有时间复杂度更低的算法,包括优化也是常数范围的优化,时间已经非常优秀了

    二、分析总结

    分析:首先,我们要搞明白,原始的算法为啥这么慢呢?因为它在一遍一遍的遍历s和t,做了很多重复工作,浪费了一些我们本该知道的信息。大大降低了效率。

    比如t长度为10,s匹配到位置5,如果t一直匹配到了t[8],到[9]才匹配错误,那s已经匹配到了位置14,下一步怎么做呢?接着从位置6开始,和t[0]开始匹配,而s位置6和t[0]甚至后面很大一部分信息我们其实都遍历过,都知道了,原始算法却还要重复匹配这些位置。所以效率极低。

    (其实很多算法都是对一般方法中的重复运算、操作做了优化,当我们写出暴力递归后,应分析出我们做了哪些重复运算,然后优化。具体优化思路我会在以后写出来。当我们可以用少量的空间就能减少大量的时间时,何乐而不为呢?)

    扯远了,下面开始进入kmp正题。

    三、基本操作

    首先扯基本操作:

    next数组概念:一个字符串中最长的相同前后缀的长度,加一。可能表达的不太好啊,举例说明:abcabcabc

    所以next[1]一直到next[9]计算的是a,ab,abc,abca,abcab直到abcabcabc的相同的最长前缀和最长后缀,加一

    注意,所谓前缀,不能包含最后一个字符,而后缀,也不能包含第一个字符,如果包含,那所有的next都成了字符串长度,也就没意义了。

    比如a,最长前后缀长度为0,原因上面刚说了,不包含。

    abca最长前后缀长度为1,即第一个和最后一个。

    abcab最长前后缀长度为2,即ab

    abcabc最长前后缀长度为3,即abc

    abcabca最长前后缀长度为4,即abca

    abcabcabc最长前后缀长度为6,即abcabc

    萌新可以把next数组看成一个黑盒,我下面会写怎么求,不过现在先继续讲主体思路。

    (感觉next数组体现了一个挺重要的思想:预处理思想。当我们不能直接求解问题时,不妨先生成一个预处理的数组,用来记录我们需要的一些信息。以后我会写这方面的专题)

     

     

     

     

    开始匹配了哦:假如主串从i位置开始和子串配,配到了i+j时配不下去了,按原来的方法,应该回到i+1,继续配,而kmp算法是这样的:

    黑色部分就是配到目前为止,前面子串中的最长相同前后缀。匹配失败以后,可以跳过我圈的那一部分开头,从主串的第二块黑色那里开始配了,这些开头肯定配不出来,这就是kmp核心的思想,至于为什么敢跳,等会讲,现在先说基本操作。

    根据定义,主串第二块黑部分和子串第一块黑部分也一样,所以直接从我划线的地方往下配就好。

    就这样操作,直到最后或配出。

     

    四、原理

    原始的kmp操作就是这样,下面讲解原理,为什么能确定跳过的那一段开头肯定配不出来呢?

    还是再画一个图来配合讲解吧。(要不然我怕表达不好唉。。好气哟)

    (懒,就是刚才的图改了改)

    咱们看普遍情况(注意,是普遍情况,任意跳过的开头位置),随便一个咱们跳过的开头,看看有没有可能配出来呢?

    竖线叫abc吧。

    主串叫s,子串交t

    请看ab线中间包含的t中的子串,它在t中是一个以t[0]为开头,比黑块更长的前缀。

    请看ab线中间包含的s中的子串,它在s中是一个以b线前一个元素为结尾,比黑块更长的后缀。

    请回想黑块定义:这是目前位置之前的子串中,最长的相同前后缀。

    请再想一想我们当初为什么能配到这里呢?

     

    这个位置之前,我们全都一样,所以多长的后缀都是相等的。

    其实就是,主数组后缀等于子数组后缀,而子数组前缀不等于子数组后缀,所以子数组前缀肯定不等与主数组后缀,也就是说,当前位置肯定配不出来

     

    这是比最长相同前后缀更长的前后缀啊兄弟。。。所以肯定不相等,如果相等,最长相同前后缀至少也是它了啊,对么?这就是能跳过的原因,这辈子不可能在这里面配出来了哦。

    主要操作和原理就这些了。。不知道解释清楚没。

    下面解释如何求解next数组:

     

    当然,一个一个看也不是不可以,在子串很短的情况下算法总时间区别不大,但是。。各位有没有一股似曾相识的感觉呢?计算next[x]还是要在t[0]-t[x-2]这个串里找最大相同前后缀啊。还是串匹配问题啊。看操作:

    (一切为了code简洁好操作),之后每个位置看看p[i-1]和p[next[i-1]]是不是相等,请回去看图,也就是第一个黑块后面那个元素和第二个黑块最后那个元素,相等,next[i]就等于next[i-1]+1。(求b,看前一个元素的最长前后缀,前一个元素和a看是不是相等。)

    若不等,继续往前看,p[i-1]是不是等于p[next[next[i-1]]],就这样一直往前跳。其实现在一看,大家是不是感觉,和s与t匹配的时候kmp主体很像啊?只是反过来跳了嘛。。。原理也是基本一样的,我就不解释了,跳过的部分也不可能配出来,你们自己证吧,不想写了。

     

    五、复杂度分析

    下面分析时间复杂度:

    主体部分,在主串上的指针,两种情况,要么配了头一个就不对,就往后走了,这时用o(1)排除了一个位置。要么就是,配了n个位置以后配不对了,那不管next数组是多少,主串上的指针总会向后走n个位置的,所以每个位置还是o(1),这样看来,主串长度是len的话,时间复杂度就是o(len)啊。

    再看next数组求解的操作,一样的啊,最多就是子串的长度那么多呗。

    所以总体时间复杂度o(m+n),原来是o(m*n)啊,向这位大神致敬,想出这么强的算法。

    六、kmp拓展题目

    (本来想放到树专题讲,但是kmp提供了很好的思路,故在本章讲述kmp方法,在树专题讲一般思路)

    输入两棵二叉树A,B,判断B是不是A的子结构。

    Oj链接

    https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

    先说一般思路,就一个一个试呗,先在A里找B的根,相等了接着往下配,全配上就行了。

    需要注意的是,子结构的定义,好好理解,不要搞错了,不太清楚定义的自己查资料。

     

    下面说利用kmp解此题的思路

    Kmp,解决字符串匹配问题,而此题是二叉树匹配问题,所以一般思路是想把树序列化,然后用kmp,但是我们有一个常识,一种遍历不能确定唯一一颗树,这是我们首先要解决的问题。

    分析为什么一个序列不能确定呢?给你个序列建立二叉树,比如1 2 3,先序吧(默认先左子树),1是根没问题,2就不一定了,可以是左子树可以是右子树,假如是左子树,那三可放的位置更不确定,这就是原因,我们不知道左子树是空,结束了,该建右子树,还是说,填在左子树。

    怎么解决这个问题?

    我请教了敬爱的老师这方法对不对,所以肯定没有问题滴。

    只要把空也表示出来就好了比如最简单的例子,先序的话就生成1 2 空 空 3 空 空

    再举一例1 2 4 空 空 空 3 空 空

    在座的各位都是大佬,应该都懂吧。

    (因为序列化和重建的方式一样,知道左子树什么时候为空,所以可以确定唯一一颗结构确定的树)

    AB树序列化以后,用kmp字符串匹配就行啦

    (当然要是为了过oj,就别秀kmp操作了,直接用系统函数,面试再自己写)

     

     

     

    整篇结束,code怎么整合,如何操作、kmp的优化,以及篇中提到的算法思想怎么养成以后可能会写。

    字数3170

     

    初稿2017/12/20

     

     18/11/26添加网址和代码:

    https://blog.csdn.net/hebtu666/article/details/84553147

    public class T1SubtreeEqualsT2 {
    
    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    
    	public static boolean isSubtree(Node t1, Node t2) {
    		String t1Str = serialByPre(t1);
    		String t2Str = serialByPre(t2);
    		return getIndexOf(t1Str, t2Str) != -1;
    	}
    
    	public static String serialByPre(Node head) {
    		if (head == null) {
    			return "#!";
    		}
    		String res = head.value + "!";
    		res += serialByPre(head.left);
    		res += serialByPre(head.right);
    		return res;
    	}
    
    	// KMP
    	public static int getIndexOf(String s, String m) {
    		if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
    			return -1;
    		}
    		char[] ss = s.toCharArray();
    		char[] ms = m.toCharArray();
    		int[] nextArr = getNextArray(ms);
    		int index = 0;
    		int mi = 0;
    		while (index < ss.length && mi < ms.length) {
    			if (ss[index] == ms[mi]) {
    				index++;
    				mi++;
    			} else if (nextArr[mi] == -1) {
    				index++;
    			} else {
    				mi = nextArr[mi];
    			}
    		}
    		return mi == ms.length ? index - mi : -1;
    	}
    
    	public static int[] getNextArray(char[] ms) {
    		if (ms.length == 1) {
    			return new int[] { -1 };
    		}
    		int[] nextArr = new int[ms.length];
    		nextArr[0] = -1;
    		nextArr[1] = 0;
    		int pos = 2;
    		int cn = 0;
    		while (pos < nextArr.length) {
    			if (ms[pos - 1] == ms[cn]) {
    				nextArr[pos++] = ++cn;
    			} else if (cn > 0) {
    				cn = nextArr[cn];
    			} else {
    				nextArr[pos++] = 0;
    			}
    		}
    		return nextArr;
    	}
    
    	public static void main(String[] args) {
    		Node t1 = new Node(1);
    		t1.left = new Node(2);
    		t1.right = new Node(3);
    		t1.left.left = new Node(4);
    		t1.left.right = new Node(5);
    		t1.right.left = new Node(6);
    		t1.right.right = new Node(7);
    		t1.left.left.right = new Node(8);
    		t1.left.right.left = new Node(9);
    
    		Node t2 = new Node(2);
    		t2.left = new Node(4);
    		t2.left.right = new Node(8);
    		t2.right = new Node(5);
    		t2.right.left = new Node(9);
    
    		System.out.println(isSubtree(t1, t2));
    
    	}
    
    }
    

     

    Manacher

    Manacher's Algorithm 马拉车算法操作及原理 

    package advanced_001;
    
    public class Code_Manacher {
    
    	public static char[] manacherString(String str) {
    		char[] charArr = str.toCharArray();
    		char[] res = new char[str.length() * 2 + 1];
    		int index = 0;
    		for (int i = 0; i != res.length; i++) {
    			res[i] = (i & 1) == 0 ? '#' : charArr[index++];
    		}
    		return res;
    	}
    
    	public static int maxLcpsLength(String str) {
    		if (str == null || str.length() == 0) {
    			return 0;
    		}
    		char[] charArr = manacherString(str);
    		int[] pArr = new int[charArr.length];
    		int C = -1;
    		int R = -1;
    		int max = Integer.MIN_VALUE;
    		for (int i = 0; i != charArr.length; i++) {
    			pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
    			while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
    				if (charArr[i + pArr[i]] == charArr[i - pArr[i]])
    					pArr[i]++;
    				else {
    					break;
    				}
    			}
    			if (i + pArr[i] > R) {
    				R = i + pArr[i];
    				C = i;
    			}
    			max = Math.max(max, pArr[i]);
    		}
    		return max - 1;
    	}
    
    	public static void main(String[] args) {
    		String str1 = "abc1234321ab";
    		System.out.println(maxLcpsLength(str1));
    	}
    
    }
    

    问题:查找一个字符串的最长回文子串

    首先叙述什么是回文子串:回文:就是对称的字符串,或者说是正反一样的

    小问题一:请问,子串和子序列一样么?请思考一下再往下看

     当然,不一样。子序列可以不连续,子串必须连续。

    举个例子,123的子串包括1,2,3,12,23,123(一个字符串本身是自己的最长子串),而它的子序列是任意选出元素组成,他的子序列有1,2,3,12,13,23,123,””,空其实也算,但是本文主要是想叙述回文,没意义。

    小问题二:长度为n的字符串有多少个子串?多少个子序列?

     子序列,每个元素都可以选或者不选,所以有2的n次方个子序列(包括空)

    子串:以一位置开头,有n个子串,以二位置开头,有n-1个子串,以此类推,我们发现,这是一个等差数列,而等差序列求和,有n*(n+1)/2个子串(不包括空)。

    (这里有一个思想需要注意,遇到等差数列求和,基本都是o(n^2)级别的)

    一、分析枚举的效率

    好,我们来分析一下暴力枚举的时间复杂度,上文已经提到过,一个字符串的所有子串,数量是o(n^2)级别,所以光是枚举出所有情况时间就是o(n^2),每一种情况,你要判断他是不是回文的话,还需要o(n),情况数和每种情况的时间,应该乘起来,也就是说,枚举时间要o(n^3),效率太低。

    二、初步优化

    思路:我们知道,回文全是对称的,每个回文串都会有自己的对称轴,而两边都对称。我们如果从对称轴开始, 向两边阔,如果总相等,就是回文,扩到两边不相等的时候,以这个对称轴向两边扩的最长回文串就找到了。

    举例:1 2 1 2 1 2 1 1 1

    我们用每一个元素作为对称轴,向两边扩

    0位置,左边没东西,只有自己;

    1位置,判断左边右边是否相等,1=1所以接着扩,然后左边没了,所以以1位置为对称轴的最长回文长度就是3;

    2位置,左右都是2,相等,继续,左右都是1,继续,左边没了,所以最长为5

    3位置,左右开始扩,1=1,2=2,1=1,左边没了,所以长度是7

    如此把每个对称轴扩一遍,最长的就是答案,对么?

    你要是点头了。。。自己扇自己两下。

    还有偶回文呢,,比如1221,123321.这是什么情况呢?这个对称轴不是一个具体的数,因为人家是偶回文。

    问题三:怎么用对称轴向两边扩的方法找到偶回文?(容易操作的)

    我们可以在元素间加上一些符号,比如/1/2/1/2/1/2/1/1/1/,这样我们再以每个元素为对称轴扩就没问题了,每个你加进去的符号都是一个可能的偶数回文对称轴,此题可解。。。因为我们没有错过任何一个可能的对称轴,不管是奇数回文还是偶数回文。

    那么请问,加进去的符号,有什么要求么?是不是必须在原字符中没出现过?请思考

     

    其实不需要的,大家想一下,不管怎么扩,原来的永远和原来的比较,加进去的永远和加进去的比较。(不举例子说明了,自己思考一下)

    好,分析一波时间效率吧,对称轴数量为o(n)级别,每个对称轴向两边能扩多少?最多也就o(n)级别,一共长度才n; 所以n*n是o(n^2)   (最大能扩的位置其实也是两个等差数列,这么理解也是o(n^2),用到刚讲的知识)

     

    小结:

    这种方法把原来的暴力枚举o(n^3)变成了o(n^2),大家想一想为什么这样更快呢?

    我在kmp一文中就提到过,我们写出暴力枚举方法后应想一想自己做出了哪些重复计算,错过了哪些信息,然后进行优化。

    看我们的暴力方法,如果按一般的顺序枚举,012345,012判断完,接着判断0123,我是没想到可以利用前面信息的方法,因为对称轴不一样啊,右边加了一个元素,左边没加。所以刚开始,老是想找一种方法,左右都加一个元素,这样就可以对上一次的信息加以利用了。

    暴力为什么效率低?永远是因为重复计算,举个例子:12121211,下标从0开始,判断1212121是否为回文串的时候,其实21212和121等串也就判断出来了,但是我们并没有记下结果,当枚举到21212或者121时,我们依旧是重新尝试了一遍。(假设主串长度为n,对称轴越在中间,长度越小的子串,被重复尝试的越多。中间那些点甚至重复了n次左右,本来一次搞定的事)

    还是这个例子,我换一个角度叙述一下,比较直观,如果从3号开始向两边扩,121,21212,最后扩到1212121,时间复杂度o(n),用枚举的方法要多少时间?如果主串长度为n,枚举尝试的子串长度为,3,5,7....n,等差数列,大家读到这里应该都知道了,等差数列求和,o(n^2)。

    三、Manacher原理

    首先告诉大家,这个算法时间可以做到o(n),空间o(n).

    好的,开始讲解这个神奇的算法。

    首先明白两个概念:

    最右回文边界R:挺好理解,就是目前发现的回文串能延伸到的最右端的位置(一个变量解决)

    中心c:第一个取得最右回文边界的那个中心对称轴;举个例子:12121,二号元素可以扩到12121,三号元素 可以扩到121,右边界一样,我们的中心是二号元素,因为它第一个到达最右边界

    当然,我们还需要一个数组p来记录每一个可能的对称轴最后扩到了哪里。

    有了这么几个东西,我们就可以开始这个神奇的算法了。

    为了容易理解,我分了四种情况,依次讲解:

     

    假设遍历到位置i,如何操作呢

     

    1)i>R:也就是说,i以及i右边,我们根本不知道是什么,因为从来没扩到那里。那没有任何优化,直接往右暴力 扩呗。

    (下面我们做i关于c的对称点,i

    2)i<R:,

    三种情况:

    i’的回文左边界在c回文左边界的里面

    i回文左边界在整体回文的外面

    i左边界和c左边界是一个元素

    (怕你忘了概念,c是对称中心,c它当初扩到了R,R是目前扩到的最右的地方,现在咱们想以i为中心,看能扩到哪里。)

    按原来o(n^2)的方法,直接向两边暴力扩。好的,魔性的优化来了。咱们为了好理解,分情况说。首先,大家应该知道的是,i’其实有人家自己的回文长度,我们用数组p记录了每个位置的情况,所以我们可以知道以i为中心的回文串有多长。

    2-1)i’的回文左边界在c回文的里面:看图

    我用这两个括号括起来的就是这两个点向两边扩到的位置,也就是i和i’的回文串,为什么敢确定i回文只有这么长?和i一样?我们看c,其实这个图整体是一个回文串啊。

    串内完全对称(1是括号左边相邻的元素,2是右括号右边相邻的元素,34同理),

     由此得出结论1:

    由整体回文可知,点2=点3,点1=点4

     

    当初i’为什么没有继续扩下去?因为点1!=点2。

    由此得出结论2:点1!=点2 

     

    因为前面两个结论,所以3!=4,所以i也就到这里就扩不动了。而34中间肯定是回文,因为整体回文,和12中间对称。

     

    2-2)i回文左边界在整体回文的外面了:看图

    这时,我们也可以直接确定i能扩到哪里,请听分析:

    当初c的大回文,扩到R为什么就停了?因为点2!=点4----------结论1;

    2为2关于i的对称点,当初i左右为什么能继续扩呢?说明点2=点2’---------结论2;

    由c回文可知2’=3,由结论2可知点2=点2’,所以2=3;

    但是由结论一可知,点2!=点4,所以推出3!=4,所以i扩到34为止了,34不等。

    而34中间那一部分,因为c回文,和i在内部的部分一样,是回文,所以34中间部分是回文。

     

    2-3)最后一种当然是i左边界和c左边界是一个元素

    点1!=点2,点2=点3,就只能推出这些,只知道34中间肯定是回文,外边的呢?不知道啊,因为不知道3和4相不相等,所以我们得出结论:点3点4内肯定是,继续暴力扩。

    原理及操作叙述完毕,不知道我讲没讲明白。。。

    四、代码及复杂度分析

     看代码大家是不是觉得不像o(n)?其实确实是的,来分析一波。。

    首先,我们的i依次往下遍历,而R(最右边界)从来没有回退过吧?其实当我们的R到了最右边,就可以结束了。再不济i自己也能把R一个一个怼到最右

    我们看情况一和四,R都是以此判断就向右一个,移动一次需要o(1)

    我们看情况二和三,直接确定了p[i],根本不用扩,直接遍历下一个元素去了,每个元素o(1).

    综上,由于i依次向右走,而R也没有回退过,最差也就是i和R都到了最右边,而让它们移动一次的代价都是o(1)的,所以总体o(n)

    可能大家看代码依旧有点懵,其实就是code整合了一下,我们对于情况23,虽然知道了它肯定扩不动,但是我们还是给它一个起码是回文的范围,反正它扩一下就没扩动,不影响时间效率的。而情况四也一样,给它一个起码是回文,不用验证的区域,然后接着扩,四和二三的区别就是。二三我们已经心中有B树,它肯定扩不动了,而四确实需要接着尝试。

    (要是写四种情况当然也可以。。但是我懒的写,太多了。便于理解分了四种情况解释,code整合后就是这样子)

     

    字数3411

    2017/12/22

     

     

    前缀树

    是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

    字典树又称为前缀树或Trie树,是处理字符串常见的数据结构。假设组成所有单词的字符仅是“a”~"z",请实现字典树结构,并包含以下四个主要功能:

    void insert(String word):添加word,可重复添加。
    void delete(String word):删除word,如果word添加过多次,仅删除一次。
    boolean search(String word):查询word是否在字典树中。
    int prefixNumber(String pre):返回以字符串pre为前缀的单词数量。
    思考:

    字典树的介绍。字典树是一种树形结构,优点是利用字符串的公共前缀来节约存储空间。

     

    基本性质:

    字典树的基本性质如下:

    • 根节点没有字符路径。除根节点外,每一个节点都被一个字符路径找到。
    • 从根节点到某一节点,将路径上经过的字符连接起来,为扫过的对应字符串。
    • 每个节点向下所有的字符路径上的字符都不同。

    也不需要记,看了实现,很自然的性质就理解了。

    每个结点内有一个指针数组,里面有二十六个指针,分别指向二十六个字母。

    如果指向某个字母的指针为空,那就是以前没有遇到过这个前缀。

     

    搜索的方法为:

    (1) 从根结点开始一次搜索;

    (2) 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索;

    (3) 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索。

    (4) 迭代过程……

    (5) 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找。

    其他操作类似处理

    插入也一样,只是转到某个子树时,没有子树,那就创建一个新节点,然后对应指针指向新节点即可。

    我们给出定义就更清楚了:

    public static class TrieNode {
    	public int path; //表示由多少个字符串共用这个节点
    	public int end;//表示有多少个字符串是以这个节点结尾的
    	public TrieNode[] map;
        //哈希表结构,key代表该节点的一条字符路径,value表示字符路径指向的节点
    	public TrieNode() {
    	    path = 0;
    	    end = 0;
    	    map = new TrieNode[26];
    	}
    }

    path和end都是有用的,接下来会说明

    insert:

    	    public static class Trie {
    	        private TrieNode root;//头
    	 
    	        public Trie() {
    	            root = new TrieNode();
    	        }
    	 
    	        public void insert(String word) {
    	            if (word == null) {
    	                return;
    	            }//空串
    	            char[] chs = word.toCharArray();
    	            TrieNode node = root;
    	            int index = 0; //哪条路
    	            for (int i = 0; i < chs.length; i++) {
    	                index = chs[i] - 'a'; //0~25
    	                if (node.map[index] == null) {
    	                    node.map[index] = new TrieNode();
    	                }//创建,继续
    	                node = node.map[index];//指向子树
    	                node.path++;//经过加1
    	            }
    	            node.end++;//本单词个数加1
    	        }
    	        public boolean search(String word) {
    	            if (word == null) {
    	                return false;
    	            }
    	            char[] chs = word.toCharArray();
    	            TrieNode node = root;
    	            int index = 0;
    	            for (int i = 0; i < chs.length; i++) {
    	                index = chs[i] - 'a';
    	                if (node.map[index] == null) {
    	                    return false;//找不到
    	                }
    	                node = node.map[index];
    	            }
    	            return node.end != 0;//end标记有没有以这个字符为结尾的字符串
    	        }

    delete: 

    	        public void delete(String word) {
                      //如果有
    	            if (search(word)) {
    	                char[] chs = word.toCharArray();
    	                TrieNode node = root;
    	                int index = 0;
    	                for (int i = 0; i < chs.length; i++) {
    	                    index = chs[i] - 'a';
    	                    if (node.map[index].path-- == 1) {//path减完之后为0
    	                        node.map[index] = null;
    	                        return;
    	                    }
    	                    node = node.map[index];//去子树
    	                }
    	                node.end--;//次数减1
    	            }
    	        }

    prefixNumber:

     public int prefixNumber(String pre) {
    	            if (pre == null) {
    	                return 0;
    	            }
    	            char[] chs = pre.toCharArray();
    	            TrieNode node = root;
    	            int index = 0;
    	            for (int i = 0; i < chs.length; i++) {
    	                index = chs[i] - 'a';
    	                if (node.map[index] == null) {
    	                    return 0;//找不到
    	                }
    	                node = node.map[index];
    	            }
    	            return node.path;//返回经过的次数即可
    	        }

    好处:

    1.利用字符串的公共前缀来节约存储空间。

    2.最大限度地减少无谓的字符串比较,查询效率比较高。例如:若要查找的字符长度是5,而总共有单词的数目是26^5=11881376,利用trie树,利用5次比较可以从11881376个可能的关键字中检索出指定的关键字,而利用二叉查找树时间复杂度是O( log2n ),所以至少要进行log211881376=23.5次比较。可以看出来利用字典树进行查找速度是比较快的。

     

    应用:

    <1.字符串的快速检索

    <2.字符串排序

    <3.最长公共前缀:abdh和abdi的最长公共前缀是abd,遍历字典树到字母d时,此时这些单词的公共前缀是abd。

    <4.自动匹配前缀显示后缀

    我们使用辞典或者是搜索引擎的时候,输入appl,后面会自动显示一堆前缀是appl的东东吧。

    那么有可能是通过字典树实现的,前面也说了字典树可以找到公共前缀,我们只需要把剩余的后缀遍历显示出来即可。

     

    相关题目:

    一个字符串类型的数组arr1,另一个字符串类型的数组arr2。

    arr2中有哪些字符,是arr1中出现的?请打印。

    arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。

    arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。

     

    后缀树/后缀数组

    字典树:https://blog.csdn.net/hebtu666/article/details/83141560

    后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。

     

    相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。

    比如单词banana,它的所有后缀显示到下面的。0代表从第一个字符为起点,终点不用说都是字符串的末尾。

    以上面的后缀,我们建立一颗后缀树。如下图,为了方便看到后缀,我没有合并相同的前缀。

    把非公共部分压缩:

    后缀树的应用:

    (1)查找某个字符串s1是否在另外一个字符串s2中:如果s1在字符串s2中,那么s1必定是s2中某个后缀串的前缀。

    (2)指定字符串s1在字符串s2中重复的次数:比如说banana是s1,an是s2,那么计算an出现的次数实际上就是看an是几个后缀串的前缀。

    (3)两个字符串S1,S2的最长公共部分(广义后缀树)

    (4)最长回文串(广义后缀树)

     

    关于后缀树的实现和应用以后再写,这次主要写后缀数组。

    在字符串处理当中,后缀树和后缀数组都是非常有力的工具。其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多。可以说,在信息学竞赛中后缀数组比后缀树要更为实用。

     

    后缀数组:就是把某个字符串的所有后缀按照字典序排序后的数组。(数组中保存起始位置就好了,结束位置一定是最后)

    先说如何计算后缀数组:

    倍增的思想,我们先把每个长度为2的子串排序,再利用结果把每个长度为4的字串排序,再利用结果排序长度为8的子串。。。直到长度大于等于串长。

    设置sa[]数组来记录排名:sa[i]代表排第i名的是第几个串。

    结果用rank[]数组返回,rank[i]记录的是起始位置为第i个字符的后缀排名第几小。

    我们开始执行过程:

    比如字符串abracadabra

    长度为2的排名:a ab ab ac ad br br ca da ra ra,他们分别排第0,1,2,2,3,4,5,5,6,7,8,8名

    sa数组就是11(空串),10(a),0(ab),7,3,5,1,8,4,6,2,9(ra排名最后)

    这样,所有长度为2的子串的排名就出来了,我们如何利用排名把长度为4的排名搞出来呢?

    abracadabra中,ab,br,ra这些串排名知道了。我们把他们两两合并为长度为4的串,进行排名。

    比如abra和brac怎么比较呢?

    用原来排名的数对来表示

    abra=ab+ra=1+8

    brac=br+ac=4+2

    对于字符串的字典序,这个例子比1和4就比出来了。

    如果第一个数一样,也就是前两个字符一样,那再比后面就可以了。

    简单说就是先比前一半字符的排名,再比后一半的排名。

    具体实现,我们可以用系统sort,传一个比较器就好了。

     

    还有需要注意,长度不可能那么凑巧是2^n,所以 一般的,k=n时,rank[i]表示从位置i开始向后n个字符的排名第几小,而剩下不足看个字符,rank[i]代表从第i个字符到最后的串的排名第几小,也就是后缀。

    保证了每一个后缀都能正确表示并排序。比如k=4时,就表示出了长度为1,2,3的后缀:a,ra,bra.这就保证了k=8时,长度为5,6,7的后缀也能被表示出来:4+1,4+2,4+3

    还有,sa[0]永远是空串,空串的排名rank[sa[0]]永远是最大。

    int n;
    int k;
    int rank[MAX_N+1];//结果(排名)数组
    int tmp[MAX_N+1];//临时数组
    //定义比较器
    bool compare(int i,int j)
    {
        if(rank[i]!=rank[j])return rank[i]<rank[j];
        //长度为k的子串的比较
        int ri=i+k<=n ? rank[i+k] : -1;
        int rj=j+k<=n ? rank[j+k] : -1;
        return ri<rj;
    }
    
    void solve(string s,int *sa)
    {
        n=s.length;
        //长度为1时,按字符码即可,长度为2时就可以直接用
        for(int i=0;i<=n;i++)
        {
            sa[i]=i;
            rank[i]=i<n ? s[i] : -1;//注意空串为最大
        }
        //由k对2k排序,直到超范围
        for(k=1;k<=n;k*=2)
        {
            sort(sa,sa+n+1,compare);
            tmp[sa[0]=0;//空串
            for(int i=1;i<=n;i++)
            {
                tmp[sa[i]]=tmp[sa[i-1]]+(compare(sa[i-1],sa[i]) ? 1 : 0);//注意有相同的
            }
            for(int i=0;i<=n;i++)
            {
                rank[i]=tmp[i];
            }
        }
    }

    具体应用以后再写。。。。。

     

    AC自动机

    今天写一下基本的AC自动机的思想原理和实现。

    Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。

    KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法。

    首先我们回忆一下KMP算法:失配之后,子串通过next数组找到应该匹配的位置,也就是最长相等前后缀。

    AC自动机也是一样,只不过是匹配到当前失配之后,找到当前字符串的后缀,和所有字符串的前缀,找出最长相等前后缀。

    就这么简单。

    当然,字典树的知识是需要了解的。

    我就默认读者都会字典树了。

    我们操作的第一步就是把那些单词做一个字典树出来,这个好理解。

     

    在AC自动机中,我们也有类似next数组的东西就是fail指针,当发现失配的字符失配的时候,跳转到fail指针指向的位置,然后再次进行匹配操作

    当前节点t有fail指针,其fail指针所指向的节点和t所代表的字符是相同的。因为t匹配成功后,我们需要去匹配t->child,发现失配,那么就从t->fail这个节点开始再次去进行匹配。

    KMP里有详细讲解过程,我就不占篇幅叙述了。

    然后说一下fail指针如何建立:

    和next数组大同小异。如果你很熟悉next数组的建立,fail指针也是一样的。

    假设当前节点为father,其孩子节点记为child。求child的Fail指针时,首先我们要找到其father的Fail指针所指向的节点,假如是t的话,我们就要看t的孩子中有没有和child节点所表示的字母相同的节点,如果有的话,这个节点就是child的fail指针,如果发现没有,则需要找father->fail->fail这个节点,然后重复上面过程,如果一直找都找不到,则child的Fail指针就要指向root。

    KMP也是一样的的操作:p[next[i-1]]p[next[next[i-1]]]这样依次往前跳啊。

     

    如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。所以我们可以利用 bfs在 Trie上面进行 fail指针的求解。流程和NEXT数组类似。

     

    匹配的时候流程也是基本一样的,请参考KMP或者直接看代码:

    HDU 2222 Keywords Search    最基本的入门题了

    就是求目标串中出现了几个模式串。

    很基础了。使用一个int型的end数组记录,查询一次。

    #include <stdio.h>
    #include <algorithm>
    #include <iostream>
    #include <string.h>
    #include <queue>
    using namespace std;
    
    struct Trie
    {
        int next[500010][26],fail[500010],end[500010];
        int root,L;
        int newnode()
        {
            for(int i = 0;i < 26;i++)
                next[L][i] = -1;
            end[L++] = 0;
            return L-1;
        }
        void init()
        {
            L = 0;
            root = newnode();
        }
        void insert(char buf[])
        {
            int len = strlen(buf);
            int now = root;
            for(int i = 0;i < len;i++)
            {
                if(next[now][buf[i]-'a'] == -1)
                    next[now][buf[i]-'a'] = newnode();
                now = next[now][buf[i]-'a'];
            }
            end[now]++;
        }
        void build()//建树
        {
            queue<int>Q;
            fail[root] = root;
            for(int i = 0;i < 26;i++)
                if(next[root][i] == -1)
                    next[root][i] = root;
                else
                {
                    fail[next[root][i]] = root;
                    Q.push(next[root][i]);
                }
            while( !Q.empty() )//建fail
            {
                int now = Q.front();
                Q.pop();
                for(int i = 0;i < 26;i++)
                    if(next[now][i] == -1)
                        next[now][i] = next[fail[now]][i];
                    else
                    {
                        fail[next[now][i]]=next[fail[now]][i];
                        Q.push(next[now][i]);
                    }
            }
        }
        int query(char buf[])//匹配
        {
            int len = strlen(buf);
            int now = root;
            int res = 0;
            for(int i = 0;i < len;i++)
            {
                now = next[now][buf[i]-'a'];
                int temp = now;
                while( temp != root )
                {
                    res += end[temp];
                    end[temp] = 0;
                    temp = fail[temp];
                }
            }
            return res;
        }
        void debug()
        {
            for(int i = 0;i < L;i++)
            {
                printf("id = %3d,fail = %3d,end = %3d,chi = [",i,fail[i],end[i]);
                for(int j = 0;j < 26;j++)
                    printf("%2d",next[i][j]);
                printf("]\n");
            }
        }
    };
    char buf[1000010];
    Trie ac;
    int main()
    {
        int T;
        int n;
        scanf("%d",&T);
        while( T-- )
        {
            scanf("%d",&n);
            ac.init();
            for(int i = 0;i < n;i++)
            {
                scanf("%s",buf);
                ac.insert(buf);
            }
            ac.build();
            scanf("%s",buf);
            printf("%d\n",ac.query(buf));
        }
        return 0;
    }

     

    数组缺失

     

    二叉树遍历

    二叉树:二叉树是每个节点最多有两个子树的树结构。

     

    本文介绍二叉树的遍历相关知识。

    我们学过的基本遍历方法,无非那么几个:前序,中序,后序,还有按层遍历等等。

    设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

    首先我们定义一颗二叉树

    typedef char ElementType;
    typedef struct TNode *Position;
    typedef Position BinTree;
    struct TNode{
        ElementType Data;
        BinTree Left;
        BinTree Right;
    };
    

    前序

    首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树

    思路:

    就是利用函数,先打印本个节点,然后对左右子树重复此过程即可。

    void PreorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        printf(" %c", BT->Data);
        PreorderTraversal(BT->Left);
        PreorderTraversal(BT->Right);
    }

     

    中序

    首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树

    思路:

    还是利用函数,先对左边重复此过程,然后打印根,然后对右子树重复。

    void InorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        InorderTraversal(BT->Left);
        printf(" %c", BT->Data);
        InorderTraversal(BT->Right);
    }

    后序

    首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根

    思路:

    先分别对左右子树重复此过程,然后打印根

    void PostorderTraversal(BinTree BT)
    {
        if(BT==NULL)return ;
        PostorderTraversal(BT->Left);
        PostorderTraversal(BT->Right);
        printf(" %c", BT->Data);
    }

    进一步思考

    看似好像很容易地写出了三种遍历。。。。。

     

    但是你真的理解为什么这么写吗?

    比如前序遍历,我们真的是按照定义里所讲的,首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。这种过程来遍历了一遍二叉树吗?

    仔细想想,其实有一丝不对劲的。。。

    再看代码:

    void Traversal(BinTree BT)//遍历
    {
    //1111111111111
        Traversal(BT->Left);
    //22222222222222
        Traversal(BT->Right);
    //33333333333333
    }

    为了叙述清楚,我给三个位置编了号 1,2,3

    我们凭什么能前序遍历,或者中序遍历,后序遍历?

    我们看,前序中序后序遍历,实现的代码其实是类似的,都是上面这种格式,只是我们分别在位置1,2,3打印出了当前节点而已啊。我们凭什么认为,在1打印,就是前序,在2打印,就是中序,在3打印,就是后序呢?不管在位置1,2,3哪里操作,做什么操作,我们利用函数遍历树的顺序变过吗?当然没有啊。。。

    都是三次返回到当前节点的过程:先到本个节点,也就是位置1,然后调用了其他函数,最后调用完了,我们开到了位置2。然后又调用别的函数,调用完了,我们来到了位置3.。然后,最后操作完了,这个函数才结束。代码里的三个位置,每个节点都被访问了三次。

    而且不管位置1,2,3打印了没有,操作了没有,这个顺序是永远存在的,不会因为你在位置1打印了,顺序就改为前序,你在位置2打印了,顺序就成了中序。

     

    为了有更直观的印象,我们做个试验:在位置1,2,3全都放入打印操作;

    我们会发现,每个节点都被打印了三次。而把每个数第一次出现拿出来,就组成了前序遍历的序列;所有数字第二次出现拿出来,就组成了中序遍历的序列。。。。

     

    其实,遍历是利用了一种数据结构:栈

    而我们这种写法,只是通过函数,来让系统帮我们压了栈而已。为什么能实现遍历?为什么我们访问完了左子树,能返回到当前节点?这都是栈的功劳啊。我们把当前节点(对于函数就是当时的现场信息)存到了栈里,记录下来,后来才能把它拿了出来,能回到以前的节点。

     

    想到这里,可能就有更深刻的理解了。

    我们能否不用函数,不用系统帮我们压栈,而是自己做一个栈,来实现遍历呢?

    先序实现思路:拿到一个节点的指针,先判断是否为空,不为空就先访问(打印)该结点,然后直接进栈,接着遍历左子树;为空则要从栈中弹出一个节点来,这个时候弹出的结点就是其父亲,然后访问其父亲的右子树,直到当前节点为空且栈为空时,结束。

    核心思路代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            //visit(p);进行操作
            st.push(p);//入栈
            p = p->lchild;左
        } 
        else//空
        {
            p = st.top();//取出
            st.pop();
            p = p->rchild;//右
        }
    }

    中序实现思路:和前序遍历一样,只不过在访问节点的时候顺序不一样,访问节点的时机是从栈中弹出元素时访问,如果从栈中弹出元素,就意味着当前节点父亲的左子树已经遍历完成,这时候访问父亲,就是中序遍历.

    (对应递归是第二次遇到)

    核心代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            st.push(p);//压入
            p = p->lchild;
        }
        else//空
        {
            p = st.top();//取出
            //visit(p);操作
            st.pop();
            p = p->rchild;
        }
    }

    后序遍历是最难的。因为要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难点。

    因为我们原来说了,后序是第三次遇到才进行操作的,所以我们很容易有这种和递归函数类似的思路:对于任一结点,将其入栈,然后沿其左子树一直往下走,一直走到没有左孩子的结点,此时该结点在栈顶,但是不能出栈访问, 因此右孩子还没访问。所以接下来按照相同的规则对其右子树进行相同的处理。访问完右孩子,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

    第二种思路:对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,或者左孩子和右孩子都已被访问过了,就可以直接访问该结点。如果有孩子未访问,将P的右孩子和左孩子依次入栈。

    网上的思路大多是第一种,所以我在这里给出第二种的大概实现吧

    首先初始化cur,pre两个指针,代表访问的当前节点和之前访问的节点。把根放入,开始执行。

    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        if((cur->lchild==NULL && cur->rchild==NULL)||(pre!=NULL && (pre==cur->lchild||pre==cur->rchild)))
        {
            //visit(cur);  如果当前结点没有孩子结点或者孩子节点都已被访问过 
            s.pop();//弹出
            pre=cur; //记录
        }
        else//分别放入右左孩子
        {
            if(cur->rchild!=NULL)
                s.push(cur->rchild);
            if(cur->lchild!=NULL)    
                s.push(cur->lchild);
        }
    }

    这两种方法,都是利用栈结构来实现的遍历,需要一定的栈空间,而其实存在一种时间O(N),空间O(1)的遍历方式,下次写了我再放链接。

     

    斗个小机灵:后序是LRD,我们其实已经知道先序是DLR,那其实我们可以用先序来实现后序啊,我们只要先序的时候把左右子树换一下:DRL(这一步很好做到),然后倒过来不就是DRL了嘛。。。。。就把先序代码改的左右反过来,然后放栈里倒过来就好了,不需要上面介绍的那些复杂的方法。。。。

     

    二叉树序列化/反序列化

    二叉树被记录成文件的过程,为二叉树的序列化

    通过文件重新建立原来的二叉树的过程,为二叉树的反序列化

    设计方案并实现。

    (已知结点类型为32位整型)

     

    思路:先序遍历实现。

    因为要写入文件,我们要把二叉树序列化为一个字符串。

    首先,我们要规定,一个结点结束后的标志:“!”

    然后就可以通过先序遍历生成先序序列了。

     

    但是,众所周知,只靠先序序列是无法确定一个唯一的二叉树的,原因分析如下:

    比如序列1!2!3!

    我们知道1是根,但是对于2,可以作为左孩子,也可以作为右孩子:

    对于3,我们仍然无法确定,应该作为左孩子还是右孩子,情况显得更加复杂:

    原因:我们对于当前结点,插入新结点是无法判断插入位置,是应该作为左孩子,还是作为右孩子。

    因为我们的NULL并未表示出来。

    如果我们把NULL也用一个符号表示出来:

    比如

    1!2!#!#!3!#!#!

    我们再按照先序遍历的顺序重建:

    对于1,插入2时,就确定要作为左孩子,因为左孩子不为空。

    然后接下来两个#,我们就知道了2的左右孩子为空,然后重建1的右子树即可。

     

    我们定义结点:

    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}

    序列化:

    	public static String serialByPre(Node head) {
    		if (head == null) {
    			return "#!";
    		}
    		String res = head.value + "!";
    		res += serialByPre(head.left);
    		res += serialByPre(head.right);
    		return res;
    	}

     

    	public static Node reconByPreString(String preStr) {
            //先把字符串转化为结点序列
    		String[] values = preStr.split("!");
    		Queue<String> queue = new LinkedList<String>();
    		for (int i = 0; i != values.length; i++) {
    			queue.offer(values[i]);
    		}
    		return reconPreOrder(queue);
    	}
    
    	public static Node reconPreOrder(Queue<String> queue) {
    		String value = queue.poll();
    		if (value.equals("#")) {
    			return null;//遇空
    		}
    		Node head = new Node(Integer.valueOf(value));
    		head.left = reconPreOrder(queue);
    		head.right = reconPreOrder(queue);
    		return head;
    	}

    这样并未改变先序遍历的时空复杂度,解决了先序序列确定唯一一颗树的问题,实现了二叉树序列化和反序列化。

     

    先序中序后序两两结合重建二叉树

    遍历是对树的一种最基本的运算,所谓遍历二叉树,就是按一定的规则和顺序走遍二叉树的所有结点,使每一个结点都被访问一次,而且只被访问一次。由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。

    设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

    先序遍历

    首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树,C语言代码如下:

    1

    2

    3

    4

    5

    6

    7

    void XXBL(tree *root){

        //DoSomethingwithroot

        if(root->lchild!=NULL)

            XXBL(root->lchild);

        if(root->rchild!=NULL)

            XXBL(root->rchild);

    }

    中序遍历

    首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树,C语言代码如下

    1

    2

    3

    4

    5

    6

    7

    8

    void ZXBL(tree *root)

    {

        if(root->lchild!=NULL)

            ZXBL(root->lchild);

            //Do something with root

        if(root->rchild!=NULL)

            ZXBL(root->rchild);

    }

    后序遍历

    首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根,C语言代码如下

    1

    2

    3

    4

    5

    6

    7

    void HXBL(tree *root){

        if(root->lchild!=NULL)

            HXBL(root->lchild);

        if(root->rchild!=NULL)

            HXBL(root->rchild);

            //Do something with root

    }

    层次遍历

    即按照层次访问,通常用队列来做。访问根,访问子女,再访问子女的子女(越往后的层次越低)(两个子女的级别相同)

     

    输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

     

    我们首先找到根结点:一定是先序遍历序列的第一个元素:1

    然后,在中序序列寻找根,把中序序列分为两个序列左子树4,7,2和右子树5,3,8,6

    把先序序列也分为两个:                                           左子树2,4,7和右子树3,5,6,8

    对左右重复同样的过程:

    先看左子树:先序序列4,7,2,说明4一定是左子树的根

    把2,4,7分为2和7两个序列,再重复过程,左边确定完毕。

    右子树同样:中序序列为5,3,8,6,先序序列为:3,5,6,8

    取先序头,3.一定是根

    把中序序列分为     5和8,6两个序列

    对应的先序序列为 5和6,8两个序列

     

    然后确定了5是3的左孩子

    对于先序序列6,8和中序序列8,6

    还是先取先序的头,6

     

    现在只有8,中序序列8在左边,是左孩子。

    结束。

    我们总结一下这种方法的过程:

    1、根据先序序列确定当前树的根(第一个元素)。

    2、在中序序列中找到根,并以根为分界分为两个序列。

    3、这样,确定了左子树元素个数,把先序序列也分为两个。

    对左右子树(对应的序列)重复相同的过程。

     

    我们把思路用代码实现:

    # -*- coding:utf-8 -*-
    # class TreeNode:
    #     def __init__(self, x):
    #         self.val = x
    #         self.left = None
    #         self.right = None
    class Solution:
        # 返回构造的TreeNode根节点
        def reConstructBinaryTree(self, pre, tin):
            # write code here/
            #pre-先序数组   tin->中序数组
            if len(pre) == 0:
                return None
            root = TreeNode(pre[0])//第一个元素为根
            pos = tin.index(pre[0])//划分左右子树
            root.left = self.reConstructBinaryTree( pre[1:1+pos], tin[:pos])
            root.right = self.reConstructBinaryTree( pre[pos+1:], tin[pos+1:])
            return root
    

    输入某二叉树的后序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

     

    思路是类似的,只是我们确定根的时候,取后序序列的最后一个元素即可。

     

    输入某二叉树的后序遍历和先序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

     

    我们直白的表述一下,前序是中左右,后序是左右中。

    所以,我们凭先序和后序序列其实是无法判断根的孩子到底是左孩子还是右孩子。

    比如先序序列1,5,后序序列是5,1

    我们只知道1是这棵树的根,但是我们不知道5是1的左孩子还是右孩子。

    我们的中序序列是左中右,才可以明确的划分出左右子树,而先序后序不可以。

     

    综上,只有,只含叶子结点或者同时有左右孩子的结点的树,才可以被先序序列后序序列确定唯一一棵树。

    最后不断划分先序和后序序列完成重建。

     

    先序中序数组推后序数组

    二叉树遍历

    所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。

     

    从二叉树的递归定义可知,一棵非空的二叉树由根结点及左、右子树这三个基本部分组成。因此,在任一给定结点上,可以按某种次序执行三个操作:

    ⑴访问结点本身(N),

    ⑵遍历该结点的左子树(L),

    ⑶遍历该结点的右子树(R)。

    以上三种操作有六种执行次序:

    NLR、LNR、LRN、NRL、RNL、RLN。

    注意:

    前三种次序与后三种次序对称,故只讨论先左后右的前三种次序。

    遍历命名

    根据访问结点操作发生位置命名:

    ① NLR:前序遍历(Preorder Traversal 亦称(先序遍历))

    ——访问根结点的操作发生在遍历其左右子树之前。

    ② LNR:中序遍历(Inorder Traversal)

    ——访问根结点的操作发生在遍历其左右子树之中(间)。

    ③ LRN:后序遍历(Postorder Traversal)

    ——访问根结点的操作发生在遍历其左右子树之后。

    注意:

    由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

     

    给出某棵树的先序遍历结果和中序遍历结果(无重复值),求后序遍历结果。

    比如

    先序序列为:1,2,4,5,3,6,7,8,9

    中序序列为:4,2,5,1,6,3,7,9,8

    方法1:我们可以重建整棵树:

    https://blog.csdn.net/hebtu666/article/details/84322113

    建议好好看这个网址,对理解这个方法有帮助。

     

    如图

    然后后序遍历得出后序序列。

     

    方法2:我们可以不用重建,直接得出:

    过程:

    1)根据当前先序数组,设置后序数组最右边的值

    2)划分出左子树的先序、中序数组和右子树的先序、中序数组

    3)对右子树重复同样的过程

    4)对左子树重复同样的过程

     

    原因:我们的后序遍历是左右中的,也就是先左子树,再右子树,再根

    举个例子:

    比如这是待填充序列:

    我们确定了根,并且根据根和中序序列划分出了左右子树,黄色部分为左子树:

    先处理右子树(其实左右中反过来就是中右左,顺着填就好了):

    我们又确定了右子树的右子树为黑色区域,然后接着填右子树的右子树的根(N)即可。

     

     

    举例说明:

    a[]先序序列为:1,2,4,5,3,6,7,8,9

    b[]中序序列为:4,2,5,1,6,3,7,9,8

    c[]后序序列为:0,0,0,0,0,0,0,0,0(0代表未确定)

    我们根据先序序列,知道根一定是1,所以后序序列:0,0,0,0,0,0,0,0,1

    从b[]中找到1,并划分数组:

              左子树的先序:2,4,5,

              中序:4,2,5

              右子树的先序:3,6,7,8,9,

              中序:6,3,7,9,8

     

    我们继续对右子树重复相同的过程:

    (图示为当前操作的树,我们是不知道这棵树的样子的,我是为了方便叙述,图片表达一下当前处理的位置)

    当前树的根一定为先序序列的第一个元素,3,所以我们知道后序序列:0,0,0,0,0,0,0,3,1

    我们继续对左右子树进行划分,中序序列为6,3,7,9,8,我们在序列中找到2,并划分为左右子树:

    左子树:

    先序序列:6

    中序序列:6

    右子树:

    先序序列:7,8,9

    中序序列:7,9,8

    我们继续对右子树重复相同的过程,也就是如图所示的这棵树:

    现在我们的后序序列为0,0,0,0,0,0,0,3,1

    这时我们继续取当前的根(先序第一个元素)放在下一个后序位置:0,0,0,0,0,0,7,3,1

    划分左右子树:

    左子树:空,也就是它

    右子树:先序8,9,中序9,8,也就是这个树

    我们继续处理右子树:先序序列为8,9,所以根为8,我们继续填后序数组0,0,0,0,0,8,7,3,1

    然后划分左右子树:

    左子树:先序:9,中序:9

    右子树:空

    对于左子树,一样,我们取头填后序数组0,0,0,0,9,8,7,3,1,然后发现左右子树都为空.

    我们就把这个小框框处理完了

    然后这棵树的右子树就处理完了,处理左子树,发现为空。这棵树也处理完了。

    这一堆就完了。我们处理以3为根的二叉树的左子树。继续填后序数组:

    0,0,0,6,9,8,7,3,1

    整棵树的右子树处理完了,左子树同样重复这个过程。

    最后4,5,2,6,9,8,7,3,1

     

    好累啊。。。。。。挺简单个事写了这么多。

    回忆一下过程:

    1)根据当前先序数组,设置后序数组最右边的值

    2)划分出左子树的先序、中序数组和右子树的先序、中序数组

    3)对右子树重复同样的过程

    4)对左子树重复同样的过程

    就这么简单

     

    先填右子树是为了数组连续填充,容易理解,先处理左子树也可以。

    最后放上代码吧

    a=[1,2,4,5,3,6,7,8,9]
    b=[4,2,5,1,6,3,7,9,8]
    l=[0,0,0,0,0,0,0,0,0]
    
    def f(pre,tin,x,y):
        #x,y为树在后序数组中对应的范围
        if pre==[]:return
        l[y]=pre[0]#根
        pos=tin.index(pre[0])#左子树元素个数
        f(pre[pos+1:],tin[pos+1:],x+pos,y-1)#处理右子树
        f(pre[1:pos+1],tin[:pos],x,x+pos-1)#处理左子树
        
    f(a,b,0,len(l)-1)
    print(l)
    

    根据数组建立平衡二叉搜索树

    它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉(搜索)树。

     

    二分:用有序数组中中间的数生成搜索二叉树的头节点,然后对数组的左右部分分别生成左右子树即可(重复过程)。

    生成的二叉树中序遍历一定还是这个序列。

     

    非常简单,不过多叙述:

    public class SortedArrayToBalancedBST {
    
    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    
    	public static Node generateTree(int[] sortArr) {
    		if (sortArr == null) {
    			return null;
    		}
    		return generate(sortArr, 0, sortArr.length - 1);
    	}
    
    	public static Node generate(int[] sortArr, int start, int end) {
    		if (start > end) {
    			return null;
    		}
    		int mid = (start + end) / 2;
    		Node head = new Node(sortArr[mid]);
    		head.left = generate(sortArr, start, mid - 1);
    		head.right = generate(sortArr, mid + 1, end);
    		return head;
    	}
    
    	// for test -- print tree
    	public static void printTree(Node head) {
    		System.out.println("Binary Tree:");
    		printInOrder(head, 0, "H", 17);
    		System.out.println();
    	}
    
    	public static void printInOrder(Node head, int height, String to, int len) {
    		if (head == null) {
    			return;
    		}
    		printInOrder(head.right, height + 1, "v", len);
    		String val = to + head.value + to;
    		int lenM = val.length();
    		int lenL = (len - lenM) / 2;
    		int lenR = len - lenM - lenL;
    		val = getSpace(lenL) + val + getSpace(lenR);
    		System.out.println(getSpace(height * len) + val);
    		printInOrder(head.left, height + 1, "^", len);
    	}
    
    	public static String getSpace(int num) {
    		String space = " ";
    		StringBuffer buf = new StringBuffer("");
    		for (int i = 0; i < num; i++) {
    			buf.append(space);
    		}
    		return buf.toString();
    	}
    
    	public static void main(String[] args) {
    		int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    		printTree(generateTree(arr));
    
    	}
    
    }
    

    java整体打印二叉树

    一个调的很好的打印二叉树的代码。

    用空格和^v来表示节点之间的关系。

    效果是这样:

    Binary Tree:
                                             v7v       
                            v6v       
                                             ^5^       
           H4H       
                                             v3v       
                            ^2^       
                                             ^1^  

     

    对于每个节点,先打印右子树,然后打印本身,然后打印左子树。

     

    public class fan {
    	public static class Node {
    		public int value;
    		Node left;
    		Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    	
    	public static void printTree(Node head) {
    		System.out.println("Binary Tree:");
    		printInOrder(head, 0, "H", 17);
    		System.out.println();
    	}
    	
    	public static void printInOrder(Node head, int height, String to, int len) {
    		if (head == null) {
    			return;
    		}
    		printInOrder(head.right, height + 1, "v", len);
    		String val = to + head.value + to;
    		int lenM = val.length();
    		int lenL = (len - lenM) / 2;
    		int lenR = len - lenM - lenL;
    		val = getSpace(lenL) + val + getSpace(lenR);
    		System.out.println(getSpace(height * len) + val);
    		printInOrder(head.left, height + 1, "^", len);
    	}
    
    	public static String getSpace(int num) {
    		String space = " ";
    		StringBuffer buf = new StringBuffer("");
    		for (int i = 0; i < num; i++) {
    			buf.append(space);
    		}
    		return buf.toString();
    	}
    
    	public static void main(String[] args) {
    		Node head = new Node(4);
    		head.left = new Node(2);
    		head.right = new Node(6);
    		head.left.left = new Node(1);
    		head.left.right = new Node(3);
    		head.right.left = new Node(5);
    		head.right.right = new Node(7);
    		printTree(head);
    
    	}
    
    }
    

    判断平衡二叉树

    平衡二叉树(Balanced Binary Tree)具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。并且左右两个子树都是一棵平衡二叉树

    (不是我们平时意义上的必须为搜索树)

    判断一棵树是否为平衡二叉树:

     

    可以暴力判断:每一颗树是否为平衡二叉树。

     

    分析:

    如果左右子树都已知是平衡二叉树,而左子树和右子树高度差绝对值不超过1,本树就是平衡的。

     

    为此我们需要的信息:左右子树是否为平衡二叉树。左右子树的高度。

     

    我们需要给父返回的信息就是:本棵树是否是平衡的、本棵树的高度。

     

    定义结点和返回值:

    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    	public static class ReturnType {
    		public int level;   //深度
    		public boolean isB;//本树是否平衡
    		
    		public ReturnType(int l, boolean is) {
    			level = l;
    			isB = is;
    		}
    	}

    我们把代码写出来:

    	// process(head, 1)
    	
    	public static ReturnType process(Node head, int level) {
    		if (head == null) {
    			return new ReturnType(level, true);
    		}
    		//取信息
    		ReturnType leftSubTreeInfo = process(head.left, level + 1);
    		if(!leftSubTreeInfo.isB) {
    			return new ReturnType(level, false);     //左子树不是->返回
    		}
    		ReturnType rightSubTreeInfo = process(head.right, level + 1);
    		if(!rightSubTreeInfo.isB) {
    			return new ReturnType(level, false);     //右子树不是->返回
    		}
    		if (Math.abs(rightSubTreeInfo.level - leftSubTreeInfo.level) > 1) {
    			return new ReturnType(level, false);     //左右高度差大于1->返回
    		}
    		
    		return new ReturnType(Math.max(leftSubTreeInfo.level, rightSubTreeInfo.level), true);
    		//返回高度和true(当前树是平衡的)
    	}

    我们不需要每次都返回高度,用一个全局变量记录即可。

    对于其它二叉树问题,可能不止一个变量信息,所以,全局记录最好都养成定义数组的习惯。

    下面贴出完整代码:

    import java.util.LinkedList;
    import java.util.Queue;
    
    public class Demo {
    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    	public static boolean isBalance(Node head) {
    		boolean[] res = new boolean[1];
    		res[0] = true;
    		getHeight(head, 1, res);
    		return res[0];
    	}
    	
    	public static class ReturnType {
    		public int level;   //深度
    		public boolean isB;//本树是否平衡
    		
    		public ReturnType(int l, boolean is) {
    			level = l;
    			isB = is;
    		}
    	}
    	
    	// process(head, 1)
    	
    	public static ReturnType process(Node head, int level) {
    		if (head == null) {
    			return new ReturnType(level, true);
    		}
    		//取信息
    		ReturnType leftSubTreeInfo = process(head.left, level + 1);
    		if(!leftSubTreeInfo.isB) {
    			return new ReturnType(level, false);     //左子树不是->返回
    		}
    		ReturnType rightSubTreeInfo = process(head.right, level + 1);
    		if(!rightSubTreeInfo.isB) {
    			return new ReturnType(level, false);     //右子树不是->返回
    		}
    		if (Math.abs(rightSubTreeInfo.level - leftSubTreeInfo.level) > 1) {
    			return new ReturnType(level, false);     //左右高度差大于1->返回
    		}
    		
    		return new ReturnType(Math.max(leftSubTreeInfo.level, rightSubTreeInfo.level), true);
    		//返回高度和true(当前树是平衡的
    	}
    
    	public static int getHeight(Node head, int level, boolean[] res) {
    		if (head == null) {
    			return level;//返回高度
    		}
    		//取信息
    		//相同逻辑
    		int lH = getHeight(head.left, level + 1, res);
    		if (!res[0]) {
    			return level;
    		}
    		int rH = getHeight(head.right, level + 1, res);
    		if (!res[0]) {
    			return level;
    		}
    		if (Math.abs(lH - rH) > 1) {
    			res[0] = false;
    		}
    		return Math.max(lH, rH);//返回高度
    	}
    
    	public static void main(String[] args) {
    		Node head = new Node(1);
    		head.left = new Node(2);
    		head.right = new Node(3);
    		head.left.left = new Node(4);
    		head.left.right = new Node(5);
    		head.right.left = new Node(6);
    		head.right.right = new Node(7);
    
    		System.out.println(isBalance(head));
    
    	}
    
    }

    判断完全二叉树

    完全二叉树的定义: 一棵二叉树,除了最后一层之外都是完全填充的,并且最后一层的叶子结点都在左边。

    https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/7773232?fr=aladdin

    百度定义

     

    思路:层序遍历二叉树

    如果一个结点,左右孩子都不为空,则pop该节点,将其左右孩子入队列

    如果一个结点,左孩子为空,右孩子不为空,则该树一定不是完全二叉树

    如果一个结点,左孩子不为空,右孩子为空;或者左右孩子都为空:::::则该节点之后的队列中的结点都为叶子节点;该树才是完全二叉树,否则返回false。

    非完全二叉树的例子(对应方法的正确性和必要性):

    下面写代码:

    定义结点:

        public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}

    方法:

    	public static boolean isCBT(Node head) {
    		if (head == null) {
    			return true;
    		}
    		Queue<Node> queue = new LinkedList<Node>();
    		boolean leaf = false;
    		Node l = null;
    		Node r = null;
    		queue.offer(head);
    		while (!queue.isEmpty()) {
    			head = queue.poll();
    			l = head.left;
    			r = head.right;
    			if ((leaf && (l != null || r != null)) || (l == null && r != null)) {
    				return false;//当前结点不是叶子结点且之前结点有叶子结点 || 当前结点有右孩子无左孩子
    			}
    			if (l != null) {
    				queue.offer(l);
    			}
    			if (r != null) {
    				queue.offer(r);
    			} else {
    				leaf = true;//无孩子即为叶子结点
    			}
    		}
    		return true;
    	}

    判断二叉搜索树

    二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树

     

    判断某棵树是否为二叉搜索树

     

    单纯判断每个结点比左孩子大比右孩子小是不对的。如图:

    15推翻了这种方法。

     

    思路:

    1)可以根据定义判断,递归进行,如果左右子树都为搜索二叉树,且左子树最大值小于根,右子树最小值大于根。成立。

    2)根据定义,中序遍历为递增序列,我们中序遍历后判断是否递增即可。

    3)我们可以在中序遍历过程中判断之前节点和当前结点的关系,不符合直接返回false即可。

    4)进一步通过morris遍历优化

    morris遍历:https://blog.csdn.net/hebtu666/article/details/83093983

     

    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    	public static boolean isBST(Node head) {
    		if (head == null) {
    			return true;
    		}
    		boolean res = true;
    		Node pre = null;
    		Node cur1 = head;
    		Node cur2 = null;
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    				}
    			}
    			if (pre != null && pre.value > cur1.value) {
    				res = false;
    			}
    			pre = cur1;
    			cur1 = cur1.right;
    		}
    		return res;
    	}

    二叉搜索树实现

    本文给出二叉搜索树介绍和实现

     

    首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

     

    那这个结构有什么有用呢?

    首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

    基本操作:

    1、插入某个数值

    2、查询是否包含某个数值

    3、删除某个数值

     

    根据实现不同,还可以实现其他很多种操作。

     

    实现思路思路:

    前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

    而删除稍微复杂一些,有下面这几种情况:

    1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

    2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

    3、以上都不满足,就把左儿子子孙中最大节点提上来。

     

    当然,反过来也是成立的,比如右儿子子孙中最小的节点。

     

    下面来叙述为什么可以这么做。

    下图中A为待删除节点。

    第一种情况:

     

    1、去掉A,把c提上来,c也是小于x的没问题。

    2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

     

    第二种情况

     

    3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

    4、同理

     

    第三种情况

     

    5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

    因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

    而黑点小于A,也就小于c,所以可以让c当右孩子

    大概证明就这样。。

    下面我们用代码实现并通过注释理解

    上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

    定义

    struct node
    {
        int val;//数据
        node *lch,*rch;//左右孩子
    };

    插入

     node *insert(node *p,int x)
     {
         if(p==NULL)//直到空就创建节点
         {
             node *q=new node;
             q->val=x;
             q->lch=q->rch=NULL;
             return p;
         }
         if(x<p->val)p->lch=insert(p->lch,x);
         else p->lch=insert(p->rch,x);
         return p;//依次返回自己,让上一个函数执行。
     }

    查找

     bool find(node *p,int x)
     {
         if(p==NULL)return false;
         else if(x==p->val)return true;
         else if(x<p->val)return find(p->lch,x);
         else return find(p->rch,x);
     }

    删除

     node *remove(node *p,int x)
     {
          if(p==NULL)return NULL;
          else if(x<p->val)p->lch=remove(p->lch,x);
          else if(x>p->val)p->lch=remove(p->rch,x);
          //以下为找到了之后
          else if(p->lch==NULL)//情况1
          {
              node *q=p->rch;
              delete p;
              return q;
          }
          else if(p->lch->rch)//情况2
          {
              node *q=p->lch;
              q->rch=p->rch;
              delete p;
              return q;
          }
          else
          {
              node *q;
              for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个
              node *r=q->rch;//最大节点
              q->rch=r->lch;//最大节点左孩子提到最大节点位置
              r->lch=p->lch;//调整黑点左孩子为B
              r->rch=p->rch;//调整黑点右孩子为c
              delete p;//删除
              return r;//返回给父
          }
          return p;
     }

    堆的简单实现

    关于堆不做过多介绍

    堆就是儿子的值一定不小于父亲的值并且树的节点都是按照从上到下,从左到右紧凑排列的树。

    (本文为二叉堆)

    具体实现并不需要指针二叉树,用数组储存并且利用公式找到父子即可。

    父:(i-1)/2

    子:i*2+1,i*2+2

    插入:首先把新数字放到堆的末尾,也就是右下角,然后查看父的数值,需要交换就交换,重复上述操作直到不需交换

    删除:把堆的第一个节点赋值为最后一个节点的值,然后删除最后一个节点,不断向下交换。

    (两个儿子:严格来说要选择数值较小的那一个)

    时间复杂度:和深度成正比,所以n个节点是O(logN)

    int heap[MAX_N],sz=0;
    //定义数组和记录个数的变量

    插入代码:

    void push(int x)
    {//节点编号
        int i=sz++;
        while(i>0)
        {
            int p=(i-1)/2;//父
            if(heap[p]<=x)break;//直到大小顺序正确跳出循环
            heap[i]=heap[p];//把父节点放下来
            i=p;
        }
        heap[i]=x;//最后把自己放上去
        
    }

    弹出:

    int pop()
    {
        int ret=heap[0];//保存好值,最后返回
        int x=heap[--sz];
        while(i*2+1<sz)
        {
            int a=i*2+1;//左孩子
            int b=i*2+2;//右孩子
            if(b<sz && heap[b]<heap[a])a=b;//找最小
            if(heap[a]>=x)break;//直到不需要交换就退出
            heap[i]=heap[a];//把儿子放上来
            i=a;
        }
        head[i]=x;//下沉到正确位置
        return ret;//返回
    }

    堆应用例题三连

    一个数据流中,随时可以取得中位数。


    题目描述:有一个源源不断地吐出整数的数据流,假设你有足够的空间来保存吐出的数。请设计一个名叫MedianHolder的结构,MedianHolder可以随时取得之前吐出所有树的中位数。

    要求:

    1.如果MedianHolder已经保存了吐出的N个数,那么任意时刻将一个新的数加入到MedianHolder的过程中,时间复杂度O(logN)。

    2.取得已经吐出的N个数整体的中位数的过程,时间复杂度O(1).

     

    看这要求就应该感觉到和堆相关吧?

    但是进一步没那么好想。

    设计的MedianHolder中有两个堆,一个是大根堆,一个是小根堆。大根堆中含有接收的所有数中较小的一半,并且按大根堆的方式组织起来,那么这个堆的堆顶就是较小一半的数中最大的那个。小根堆中含有接收的所有数中较大的一半,并且按小根堆的方式组织起来,那么这个堆的堆顶就是较大一半的数中最小的那个。

    例如,如果已经吐出的数为6,1,3,0,9,8,7,2.

    较小的一半为:0,1,2,3,那么3就是这一半的数组成的大根堆的堆顶

    较大的一半为:6,7,8,9,那么6就是这一半的数组成的小根堆的堆顶

    因为此时数的总个数为偶数,所以中位数就是两个堆顶相加,再除以2.

    如果此时新加入一个数10,那么这个数应该放进较大的一半里,所以此时较大的一半数为6,7,8,9,10,此时6依然是这一半的数组成的小根堆的堆顶,因为此时数的总个数为奇数,所以中位数应该是正好处在中间位置的数,而此时大根堆有4个数,小根堆有5个数,那么小根堆的堆顶6就是此时的中位数。

    如果此时又新加入一个数11,那么这个数也应该放进较大的一半里,此时较大一半的数为:6,7,8,9,10,11.这个小根堆大小为6,而大根堆的大小为4,所以要进行如下调整:

    1.如果大根堆的size比小根堆的size大2,那么从大根堆里将堆顶元素弹出,并放入小根堆里

    2,如果小根堆的size比大根堆的size大2,那么从小根堆里将堆顶弹出,并放入大根堆里。

    经过这样的调整之后,大根堆和小根堆的size相同。

    总结如下:

    大根堆每时每刻都是较小的一半的数,堆顶为这一堆数的最大值
    小根堆每时每刻都是较大的一半的数,堆顶为这一堆数的最小值
    新加入的数根据与两个堆堆顶的大小关系,选择放进大根堆或者小根堆里(或者放进任意一个堆里)
    当任何一个堆的size比另一个size大2时,进行如上调整的过程。


    这样随时都可以知道已经吐出的所有数处于中间位置的两个数是什么,取得中位数的操作时间复杂度为O(1),同时根据堆的性质,向堆中加一个新的数,并且调整堆的代价为O(logN)。
     

    import java.util.Arrays;
    import java.util.Comparator;
    import java.util.PriorityQueue;
     
    /**
     * 随时找到数据流的中位数
     * 思路:
     * 利用一个大根堆和一个小根堆去保存数据,保证前一半的数放在大根堆,后一半的数放在小根堆
     * 在添加数据的时候,不断地调整两个堆的大小,使得两个堆保持平衡
     * 要取得的中位数就是两个堆堆顶的元素
     */
    public class MedianQuick {
        public static class MedianHolder {
            private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new MaxHeapComparator());
            private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(new MinHeapComparator());
     
            /**
             * 调整堆的大小
             * 当两个堆的大小差值变大时,从数据多的堆中弹出一个数据进入另一个堆中
             */
            private void modifyTwoHeapsSize() {
                if (this.maxHeap.size() == this.minHeap.size() + 2) {
                    this.minHeap.add(this.maxHeap.poll());
                }
                if (this.minHeap.size() == this.maxHeap.size() + 2) {
                    this.maxHeap.add(this.minHeap.poll());
                }
            }
     
            /**
             * 添加数据的过程
             *
             * @param num
             */
            public void addNumber(int num) {
                if (this.maxHeap.isEmpty()) {
                    this.maxHeap.add(num);
                    return;
                }
                if (this.maxHeap.peek() >= num) {
                    this.maxHeap.add(num);
                } else {
                    if (this.minHeap.isEmpty()) {
                        this.minHeap.add(num);
                        return;
                    }
                    if (this.minHeap.peek() > num) {
                        this.maxHeap.add(num);
                    } else {
                        this.minHeap.add(num);
                    }
                }
                modifyTwoHeapsSize();
            }
     
            /**
             * 获取中位数
             *
             * @return
             */
            public Integer getMedian() {
                int maxHeapSize = this.maxHeap.size();
                int minHeapSize = this.minHeap.size();
                if (maxHeapSize + minHeapSize == 0) {
                    return null;
                }
                Integer maxHeapHead = this.maxHeap.peek();
                Integer minHeapHead = this.minHeap.peek();
                if (((maxHeapSize + minHeapSize) & 1) == 0) {
                    return (maxHeapHead + minHeapHead) / 2;
                }
                return maxHeapSize > minHeapSize ? maxHeapHead : minHeapHead;
            }
        }
     
        /**
         * 大根堆比较器
         */
        public static class MaxHeapComparator implements Comparator<Integer> {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o2 > o1) {
                    return 1;
                } else {
                    return -1;
                }
            }
        }
     
        /**
         * 小根堆比较器
         */
        public static class MinHeapComparator implements Comparator<Integer> {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o2 < o1) {
                    return 1;
                } else {
                    return -1;
                }
            }
        }
     
        // for test
        public static int[] getRandomArray(int maxLen, int maxValue) {
            int[] res = new int[(int) (Math.random() * maxLen) + 1];
            for (int i = 0; i != res.length; i++) {
                res[i] = (int) (Math.random() * maxValue);
            }
            return res;
        }
     
        // for test, this method is ineffective but absolutely right
        public static int getMedianOfArray(int[] arr) {
            int[] newArr = Arrays.copyOf(arr, arr.length);
            Arrays.sort(newArr);
            int mid = (newArr.length - 1) / 2;
            if ((newArr.length & 1) == 0) {
                return (newArr[mid] + newArr[mid + 1]) / 2;
            } else {
                return newArr[mid];
            }
        }
     
        public static void printArray(int[] arr) {
            for (int i = 0; i != arr.length; i++) {
                System.out.print(arr[i] + " ");
            }
            System.out.println();
        }
     
        public static void main(String[] args) {
            boolean err = false;
            int testTimes = 200000;
            for (int i = 0; i != testTimes; i++) {
                int len = 30;
                int maxValue = 1000;
                int[] arr = getRandomArray(len, maxValue);
                MedianHolder medianHold = new MedianHolder();
                for (int j = 0; j != arr.length; j++) {
                    medianHold.addNumber(arr[j]);
                }
                if (medianHold.getMedian() != getMedianOfArray(arr)) {
                    err = true;
                    printArray(arr);
                    break;
                }
            }
            System.out.println(err ? "Oops..what a fuck!" : "today is a beautiful day^_^");
     
        }
    }
    

    金条

     

    一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。一群人想整分整块金条,怎么分最省铜板?
    例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60,金条要分成10,20,30三个部分。如果,先把长度60的金条分成10和50,花费60,再把长度为50的金条分成20和30,花费50,一共花费110个铜板。

    但是如果,先把长度60的金条分成30和30,花费60,再把长度30金条分成10和30,花费30,一共花费90个铜板。

    输入一个数组,返回分割的最小代价。

    首先我们要明白一点:不管合并策略是什么我们一共会合并n-1次,这个次数是不会变的。

    我们要做的就是每一次都做最优选择。

    合为最优?

    最小的两个数合并就是最优。

    所以

    1)首先构造小根堆

    2)每次取最小的两个数(小根堆),使其代价最小。并将其和加入到小根堆中

    3)重复(2)过程,直到最后堆中只剩下一个节点。

     

    花费为每次花费的累加。

    代码略。

     

    项目最大收益(贪心问题)


    输入:参数1,正数数组costs,参数2,正数数组profits,参数3,正数k,参数4,正数m

    costs[i]表示i号项目的花费profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润),k表示你不能并行,只能串行的最多做k个项目,m表示你初始的资金。

    说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。

    输出:你最后获得的最大钱数。

    思考:给定一个初始化投资资金,给定N个项目,想要获得其中最大的收益,并且一次只能做一个项目。这是一个贪心策略的问题,应该在能做的项目中选择收益最大的。

    按照花费的多少放到一个小根堆里面,然后要是小根堆里面的头节点的花费少于给定资金,就将头节点一个个取出来,放到按照收益的大根堆里面。然后做大根堆顶的项目即可。

     并查集实现

    并查集是什么东西?

    它是用来管理元素分组情况的一种数据结构。

    他可以高效进行两个操作:

    1. 查询a,b是否在同一组
    2. 合并a和b所在的组

    萌新可能不知所云,这个结构到底有什么用?

    经分析,并查集效率之高超乎想象,对n个元素的并查集进行一次操作的复杂度低于O(logn)

     

    我们先说并查集是如何实现的:

    也是使用树形结构,但不是二叉树。

    每个元素就是一个结点,每组都是一个树。

    无需关注它的形状,或哪个节点具体在哪个位置。

     

    初始化:

    我们现在有n个结点,也就是n个元素。

     

    合并:

    然后我们就可以合并了,合并方法就是把一个根放到另一颗树的下面,也就是整棵树作为人家的一个子树。

     

    查询:

    查询两个结点是否是同一组,需要知道这两个结点是不是在一棵树上,让他们分别沿着树向根找,如果两个元素最后走到一个根,他们就在一组。

     

    当然,树形结构都存在退化的缺点,对于每种结构,我们都有自己的优化方法,下面我们说明如何避免退化。

    1. 记录每一棵树的高度,合并操作时,高度小的变为高度大的子树即可。
    2. 路径压缩:对于一个节点,只要走到了根节点,就不必再在很深的地方,直接改为连着根即可。进一步优化:其实每一个经过的节点都可以直接连根。

    这样查询的时候就能很快地知道根是谁了。

     

    下面上代码实现:

    和很多树结构一样,我们没必要真的模拟出来,数组中即可。

    int p[MAX_N];//父亲
    int rank[MAX_N];//高度
    //初始化
    void gg(int n)
    {
        for(int i=0;i<n;i++)
        {
            p[i]=i;//父是自己代表是根
            rank[i]=0;
        }
    }
    //查询根
    int find(int x)
    {
        if(p[x]==x)return x;
        return p[x]=find(p[x])//不断把经过的结点连在根
    }
    //判断是否属于同一组
    bool judge(int x,int y)
    {
        return find(x)==find(y);//查询结果一样就在一组
    }
    //合并
    void unite(int x,int y)
    {
        if(x==y)return;
        if(rank[x]<rank[y])p[x]=y;//深度小,放在大的下面
        else
        {
            p[y]=x;
            if(rank[x]=rank[y])rank[x]++;//一样,y放x后,x深度加一
        }
    }

    实现很简单,应用有难度,以后有时间更新题。

    并查集入门三连:HDU1213 POJ1611 POJ2236

    HDU1213

    http://acm.hdu.edu.cn/showproblem.php?pid=1213

    问题描述

    今天是伊格纳修斯的生日。他邀请了很多朋友。现在是晚餐时间。伊格纳修斯想知道他至少需要多少桌子。你必须注意到并非所有的朋友都互相认识,而且所有的朋友都不想和陌生人呆在一起。

    这个问题的一个重要规则是,如果我告诉你A知道B,B知道C,那意味着A,B,C彼此了解,所以他们可以留在一个表中。

    例如:如果我告诉你A知道B,B知道C,D知道E,所以A,B,C可以留在一个表中,D,E必须留在另一个表中。所以Ignatius至少需要2张桌子。

    输入

    输入以整数T(1 <= T <= 25)开始,表示测试用例的数量。然后是T测试案例。每个测试用例以两个整数N和M开始(1 <= N,M <= 1000)。N表示朋友的数量,朋友从1到N标记。然后M行跟随。每一行由两个整数A和B(A!= B)组成,这意味着朋友A和朋友B彼此了解。两个案例之间会有一个空白行。

     

    对于每个测试用例,只输出Ignatius至少需要多少个表。不要打印任何空白。

    样本输入

    2

    5 3

    1 2

    2 3

    4 5

     

    5 1

    2 5

    样本输出

    2

    4

    并查集基础题

    #include<cstdio>
    #include<iostream>
    using namespace std;
    int fa[1005];
    int n,m;
    void init()//初始化
    {
        for(int i=0;i<1005;i++)
            fa[i]=i;
    }
    int find(int x)//寻根
    {
        if(fa[x]!=x)
            fa[x]=find(fa[x]);
        return fa[x];
    }
    void union(int x,int y)//判断、合并
    {
        int a=find(x),b=find(y);
        if(a!=b)
             fa[b]=a;
    }
    int main()
    {
        int t;
        scanf("%d",&t);
        while(t--)
        {
            int a,b,cnt=0;
            scanf("%d%d",&n,&m);
            init();
            for(int i=1;i<=m;i++)//合并
            {
                scanf("%d%d",&a,&b);
                union(a,b);
            }
            for(int i=1;i<=n;i++)//统计
            {
                find(i);
                if(find(i)==i)
                    cnt++;
            }
            printf("%d\n",cnt);
        }
        return 0;
    }
    

    POJ1611

    http://poj.org/problem?id=1611

    描述

    严重急性呼吸系统综合症(SARS)是一种病因不明的非典型肺炎,在2003年3月中旬被认为是一种全球性威胁。为了尽量减少对他人的传播,最好的策略是将嫌疑人与其他嫌疑人分开。 
    在Not-Spreading-Your-Sickness University(NSYSU),有许多学生团体。同一组中的学生经常互相交流,学生可以加入几个小组。为了防止可能的SARS传播,NSYSU收集所有学生组的成员列表,并在其标准操作程序(SOP)中制定以下规则。 
    一旦组中的成员是嫌疑人,该组中的所有成员都是嫌疑人。 
    然而,他们发现,当学生被认定为嫌疑人时,识别所有嫌疑人并不容易。你的工作是编写一个找到所有嫌疑人的程序。

    输入

    输入文件包含几种情况。每个测试用例以一行中的两个整数n和m开始,其中n是学生数,m是组的数量。您可以假设0 <n <= 30000且0 <= m <= 500.每个学生都使用0到n-1之间的唯一整数进行编号,并且最初学生0在所有情况下都被识别为嫌疑人。该行后面是组的m个成员列表,每组一行。每行以整数k开头,表示组中的成员数。在成员数量之后,有k个整数代表该组中的学生。一行中的所有整数由至少一个空格分隔。 


    n = 0且m = 0的情况表示输入结束,无需处理。

     

    对于每种情况,输出一行中的嫌疑人数量。

    样本输入

    100 4
    2 1 2
    5 10 13 11 12 14
    2 0 1
    2 99 2
    200 2
    1 5
    5 1 2 3 4 5
    1 0
    0 0

    样本输出

    4
    1
    1

     

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include <string>
    using namespace std;
    int a[30001],pre[30001];
    int find(int x)//寻根
    {
    	 if(pre[x]==x)
            return x;
        else
            return pre[x]=find(pre[x]);
    }
    void union(int x, int y)//合并
    {
    	int fx = find(x), fy = find(y);
    	if (fx != fy)
    		pre[fy] = fx;
    }
    
    int main()
    {
    	int n,m;
    	while (scanf("%d%d", &n, &m) != EOF && (n || m))
    	{
    		int sum = 0;
    		for (int i = 0; i < n; i++)//初始化
    			pre[i] = i;
    		for (int i = 0; i < m; i++)
    		{
    			int k;
    			scanf("%d", &k);
    			if (k >= 1)
    			{
    				scanf("%d", &a[0]);
    				for (int j = 1; j < k; j++)
    				{
    					scanf("%d", &a[j]);//接收
    					union(a[0], a[j]);//和0号一组
    				}
    			}
    		}
    		for (int i = 0; i < n; i++)//统计
    			if (find(i) ==pre[0])
    				sum++;
    		printf("%d\n", sum);
    	}
    	return 0;
    }
    

     POJ2236

    http://poj.org/problem?id=2236

    描述

    地震发生在东南亚。ACM(亚洲合作医疗团队)已经与膝上电脑建立了无线网络,但是一次意外的余震袭击,网络中的所有计算机都被打破了。计算机一个接一个地修复,网络逐渐开始工作。由于硬件限制,每台计算机只能直接与距离它不远的计算机进行通信。但是,每台计算机都可以被视为两台计算机之间通信的中介,也就是说,如果计算机A和计算机B可以直接通信,或者计算机C可以与A和A进行通信,则计算机A和计算机B可以进行通信。 B. 

    在修复网络的过程中,工作人员可以随时进行两种操作,修复计算机或测试两台计算机是否可以通信。你的工作是回答所有的测试操作。 

    输入

    第一行包含两个整数N和d(1 <= N <= 1001,0 <= d <= 20000)。这里N是计算机的数量,编号从1到N,D是两台计算机可以直接通信的最大距离。在接下来的N行中,每行包含两个整数xi,yi(0 <= xi,yi <= 10000),这是N台计算机的坐标。从第(N + 1)行到输入结束,有一些操作,这些操作是一个接一个地执行的。每行包含以下两种格式之一的操作: 
    1。“O p”(1 <= p <= N),表示修复计算机p。 
    2.“S p q”(1 <= p,q <= N),这意味着测试计算机p和q是否可以通信。 

    输入不会超过300000行。 

    产量

    对于每个测试操作,如果两台计算机可以通信则打印“SUCCESS”,否则打印“FAIL”。

    样本输入

    4 1
    0 1
    0 2
    0 3
    0 4
    O 1
    O 2
    O 4
    S 1 4
    O 3
    S 1 4

    样本输出

    FAIL
    SUCCESS

     思路:对每次修好的电脑对其它已经修好的电脑遍历,如果距离小于等于最大通信距离就将他们合并。

    注意

      1、坐标之后给出的计算机编号都是n+1的。例如O 3,他实际上修理的是编号为2的计算机,因为计算机是从0开始编号的。

      2、比较距离的时候注意要用浮点数比较,否则会WA。

      3、"FAIL"不要写成"FALL"。

      4、字符串输入的时候注意处理好回车,空格等情况。

      5、注意N的范围(1 <= N <= 1001),最大是1001,不是1000。是个小坑,数组开小了可能会错哦。

     

    #include <iostream>
    #include <stdio.h>
    #include <cmath>
    using namespace std;
    
    #define MAXN 1010
    
    int dx[MAXN],dy[MAXN];    //坐标
    int par[MAXN];    //x的父节点
    int repair[MAXN] ={0};
    int n;
    
    void Init()//初始化
    {
        int i;
        for(i=0;i<=n;i++)
            par[i] = i;
    }
    
    int Find(int x)//寻根
    {
        if(par[x]!=x)
            par[x] = Find(par[x]);
        return par[x];
    }
    
    void Union(int x,int y)//合并
    {
        par[Find(x)] = Find(y);
    }
    
    int Abs(int n)//绝对值
    {
        return n>0?n:-n;
    }
    
    double Dis(int a,int b)//坐标
    {
        return sqrt( double(dx[a]-dx[b])*(dx[a]-dx[b]) + (dy[a]-dy[b])*(dy[a]-dy[b]) );
    }
    
    int main()
    {
        int d,i;
    
        //初始化
        scanf("%d%d",&n,&d);
        Init();
    
        //输入坐标
        for(i=0;i<n;i++){
            scanf("%d%d",&dx[i],&dy[i]);
        }
        
        //操作
        char cmd[2];
        int p,q,len=0;
        while(scanf("%s",cmd)!=EOF)
        {
            switch(cmd[0])
            {
                case 'O':
                    scanf("%d",&p);
                    p--;
                    repair[len++] = p;
                    for(i=0;i<len-1;i++)    //遍历所有修过的计算机,看能否联通
                        if( repair[i]!=p && Dis(repair[i],p)<=double(d) )
                            Union(repair[i],p);
                    break;
                case 'S':
                    scanf("%d%d",&p,&q);
                    p--,q--;
                    if(Find(p)==Find(q))    //判断
                        printf("SUCCESS\n");
                    else 
                        printf("FAIL\n");
                default:
                    break;
            }
        }
    
        return 0;
    }

    线段树简单实现

    首先,线段树是一棵满二叉树。(每个节点要么有两个孩子,要么是深度相同的叶子节点)

    每个节点维护某个区间,根维护所有的。

     转存失败重新上传取消 

    如图,区间是二分父的区间。

    当有n个元素,初始化需要o(n)时间,对区间操作需要o(logn)时间。

    下面给出维护区间最小值的思路和代码

    功能:一样的,依旧是查询和改值。

    查询[s,t]之间最小的数。修改某个值。

     

    从下往上,每个节点的值为左右区间较小的那一个即可。

    这算是简单动态规划思想,做到了o(n),因为每个节点就访问一遍,而叶子节点一共n个,所以访问2n次即可。

    如果利用深搜初始化,会到o(nlogn)。

    https://blog.csdn.net/hebtu666/article/details/81777273

    有介绍

    那我们继续说,如何查询。

    不要以为它是二分区间就只能查二分的那些区间,它能查任意区间。

    比如上图,求1-7的最小值,查询1-4,5-6,7-7即可。

    下面说过程:

    递归实现:

    如果要查询的区间和本节点区间没有重合,返回一个特别大的数即可,不要影响其他结果。

    如果要查询的区间完全包含了本节点区间,返回自身的值

    都不满足,对左右儿子做递归,返回较小的值。

     

    如何更新?

    更新ai,就要更新所有包含ai的区间。

    可以从下往上不断更新,把节点的值更新为左右孩子较小的即可。

     

    代码实现和相关注释:

    注:没有具体的初始化,dp思路写过了,实在不想写了

    初始全为INT_MAX

    const int MAX_N=1<<7;
    int n;
    int tree[2*MAX_N-1];
    //初始化
    void gg(int nn)
    {
        n=1;
        while(n<nn)n*=2;//把元素个数变为2的n次方
        for(int i=0;i<2*n-1;i++)tree[i]=INTMAX;//所有值初始化为INTMAX
    }
    
    //查询区间最小值
    int get(int a,int b,int k,int l,int r)//l和r是区间,k是节点下标,求[a,b)最小值
    {
        if(a>=r || b<=l)return INTMAX;//情况1
        if(a<=l || b<=b)return tree[k];//情况2
        int ll=get(a,b,k*2+1,l,(l+r)/2);//以前写过,左孩子公式
        int rr=get(a,b,k*2+2,(l+r)/2,r);//右孩子
        return min(ll,rr);
    }
    
    //更新
    void update(int k,int a)//第k个值更新为a
    {
        //本身
        k+=n-1;//加上前面一堆节点数
        tree[k]=a;
        //开始向上
        while(k>0)
        {
            tree[k]=min(tree[2*k+1],tree[2*k+2]);
            k=(k-1)/2//父的公式,也写过
        }
    }

     树状数组实现

    树状数组能够完成如下操作:

    给一个序列a0-an

    计算前i项和

    对某个值加x

    时间o(logn)

     

    注意:有人觉得前缀和就行了,但是你还要维护啊,改变某个值,一个一个改变前缀和就是o(n)了。

    线段树树状数组的题就是这样,维护一个树,比较容易看出来。

     

     

    线段树:

    https://blog.csdn.net/hebtu666/article/details/82691008

    如果使用线段树,只需要对网址中的实现稍微修改即可。以前维护最小值,现在维护和而已。

    注意:要求只是求出前i项,而并未给定一个区间,那我们就能想出更快速、方便的方法。

    对于任意一个节点,作为右孩子,如果求和时被用到,那它的左兄弟一定也会被用到,那我们就没必要再用右孩子,因为用他们的父就可以了。

    这样一来,我们就可以把所有有孩子全部去掉

    把剩下的节点编号。

     转存失败重新上传取消 

    如图,可以发现一些规律:1,3,5,7,9等奇数,区间长度都为1

    6,10,14等长度为2

    ........................

    如果我们吧编号换成二进制,就能发现,二进制以1结尾的数字区间长度为1,最后有一个零的区间为2,两个零的区间为4.

    我们利用二进制就能很容易地把编号和区间对应起来。

     

    计算前i项和。

    需要把当前编号i的数值加进来,把i最右边的1减掉,直到i变为0.

    二进制最后一个1可以通过i&-i得到。

     

    更新:

    不断把当前位置i加x,把i的二进制最低非零位对应的幂加到i上。

    下面是代码:

    思想想出来挺麻烦,代码实现很简单,我都不知道要注释点啥

    向发明这些东西的大佬们致敬

    int bit[MAX_N+1]
    int n;
    
    int sum(int i)
    {
        int gg=0;
        while(i>0)
        {
            gg+=bit[i];
            i-=i&-i;
        }
        return gg;
    }
    
    void add(int i,int x)
    {
        while(i<=n)
        {
            bit[i]+=x;
            i+=i&-i;
        }
    }

    最大搜索子树

    给定一个二叉树的头结点,返回最大搜索子树的大小。

     

    我们先定义结点:

        public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}

    分析:

    直接判断每个节点左边小右边大是不对滴

     

    可以暴力判断所有的子树,就不说了。

     

    最大搜索子树可能性:

    第一种可能性,以node为头的结点的最大二叉搜索子树可能来自它左子树;
    第二种可能性,以node为头的结点的最大二叉搜索子树可能来自它右子树;
    第三种可能性,左树整体是搜索二叉树,右树整体也是搜索二叉树,而且左树的头是node.left,右树的头是node.right,且左树的最大值< node.value,右树的最小值 > node.value, 那么以我为头的整棵树都是搜索二叉树;
     

    第三种可能性的判断,需要的信息有:左子树的最大值、右子树的最小值、左子树是不是搜索二叉树、右子树是不是搜索二叉树

    还有左右搜索二叉树的最大深度。

    我们判断了自己,并不知道自己是哪边的子树,我们要返回自己的最大值和最小值。

    这样,定义一个返回类型:

        public static class ReturnType{
    		public int size;//最大搜索子树深度
    		public Node head;//最大搜索子树的根
    		public int min;//子树最小
    		public int max;//子树最大
    		
    		public ReturnType(int a, Node b,int c,int d) {
    			this.size =a;
    			this.head = b;
    			this.min = c;
    			this.max = d;
    		}
    	}

    然后开始写代码:

    注意:

    1)NULL返回深度0,头为NULL,最大值最小值返回系统最大和最小,这样才不会影响别的判断。

    	public static ReturnType process(Node head) {
    		if(head == null) {
    			return new ReturnType(0,null,Integer.MAX_VALUE, Integer.MIN_VALUE);
    		}
    		
    		Node left = head.left;//取信息
    		ReturnType leftSubTressInfo = process(left);
    		Node right = head.right;
    		ReturnType rightSubTressInfo = process(right);
    		
    		int includeItSelf = 0;
    		if(leftSubTressInfo.head == left //            左子树为搜索树
    				&&rightSubTressInfo.head == right//    右子树为搜索树
    				&& head.value > leftSubTressInfo.max// 左子树最大值小于当前节点
    				&& head.value < rightSubTressInfo.min//右子树最小值大于当前节点
    				) {
    			includeItSelf = leftSubTressInfo.size + 1 + rightSubTressInfo.size;//当前节点为根的二叉树为搜索树
    		}
    		
    		int p1 = leftSubTressInfo.size;
    		int p2 = rightSubTressInfo.size;
    		
    		int maxSize = Math.max(Math.max(p1, p2), includeItSelf);//最大搜索树深度
    		
    		Node maxHead = p1 > p2 ? leftSubTressInfo.head : rightSubTressInfo.head;
    
    		if(maxSize == includeItSelf) {
    			maxHead = head;
    		}//最大搜索树的根:来自左子树、来自右子树、本身
    		
    		return new ReturnType(
    				maxSize,                                                                     //深度
    				maxHead,                                                                     //根
    				Math.min(Math.min(leftSubTressInfo.min,rightSubTressInfo.min),head.value),    //最小
    				Math.max(Math.max(leftSubTressInfo.max,rightSubTressInfo.max),head.value));	//最大
    	}

    可以进一步改进:

    空间浪费比较严重

    其实返回值为三个int,一个node,我们可以把三个int合起来,用全局数组记录,函数只返回node(搜索树的根)即可。

    给出完整代码:

    public class BiggestSubBSTInTree {
    
    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    
    	public static Node biggestSubBST(Node head) {
    		int[] record = new int[3]; // 0->size, 1->min, 2->max
    		return posOrder(head, record);
    	}
    	
    	public static class ReturnType{
    		public int size;//最大搜索子树深度
    		public Node head;//最大搜索子树的根
    		public int min;//子树最小
    		public int max;//子树最大
    		
    		public ReturnType(int a, Node b,int c,int d) {
    			this.size =a;
    			this.head = b;
    			this.min = c;
    			this.max = d;
    		}
    	}
    	
    	public static ReturnType process(Node head) {
    		if(head == null) {
    			return new ReturnType(0,null,Integer.MAX_VALUE, Integer.MIN_VALUE);
    		}
    		
    		Node left = head.left;//取信息
    		ReturnType leftSubTressInfo = process(left);
    		Node right = head.right;
    		ReturnType rightSubTressInfo = process(right);
    		
    		int includeItSelf = 0;
    		if(leftSubTressInfo.head == left //            左子树为搜索树
    				&&rightSubTressInfo.head == right//    右子树为搜索树
    				&& head.value > leftSubTressInfo.max// 左子树最大值小于当前节点
    				&& head.value < rightSubTressInfo.min//右子树最小值大于当前节点
    				) {
    			includeItSelf = leftSubTressInfo.size + 1 + rightSubTressInfo.size;//当前节点为根的二叉树为搜索树
    		}
    		
    		int p1 = leftSubTressInfo.size;
    		int p2 = rightSubTressInfo.size;
    		
    		int maxSize = Math.max(Math.max(p1, p2), includeItSelf);//最大搜索树深度
    		
    		Node maxHead = p1 > p2 ? leftSubTressInfo.head : rightSubTressInfo.head;
    		if(maxSize == includeItSelf) {
    			maxHead = head;
    		}//最大搜索树的根:来自左子树、来自右子树、本身
    		
    		return new ReturnType(
    				maxSize,                                                                     //深度
    				maxHead,                                                                     //根
    				Math.min(Math.min(leftSubTressInfo.min,rightSubTressInfo.min),head.value),   //最小
    				Math.max(Math.max(leftSubTressInfo.max,rightSubTressInfo.max),head.value));	 //最大
    	}
    	
    	
    	
    
    	public static Node posOrder(Node head, int[] record) {
    		if (head == null) {
    			record[0] = 0;
    			record[1] = Integer.MAX_VALUE;
    			record[2] = Integer.MIN_VALUE;
    			return null;
    		}
    		int value = head.value;
    		Node left = head.left;
    		Node right = head.right;
    		Node lBST = posOrder(left, record);
    		int lSize = record[0];
    		int lMin = record[1];
    		int lMax = record[2];
    		Node rBST = posOrder(right, record);
    		int rSize = record[0];
    		int rMin = record[1];
    		int rMax = record[2];
    		record[1] = Math.min(rMin, Math.min(lMin, value)); // lmin, value, rmin -> min 
    		record[2] = Math.max(lMax, Math.max(rMax, value)); // lmax, value, rmax -> max
    		if (left == lBST && right == rBST && lMax < value && value < rMin) {
    			record[0] = lSize + rSize + 1;//修改深度
    			return head;                  //返回根
    		}//满足当前构成搜索树的条件
    		record[0] = Math.max(lSize, rSize);//较大深度
    		return lSize > rSize ? lBST : rBST;//返回较大搜索树的根
    	}
    
    	// for test -- print tree
    	public static void printTree(Node head) {
    		System.out.println("Binary Tree:");
    		printInOrder(head, 0, "H", 17);
    		System.out.println();
    	}
    
    	public static void printInOrder(Node head, int height, String to, int len) {
    		if (head == null) {
    			return;
    		}
    		printInOrder(head.right, height + 1, "v", len);
    		String val = to + head.value + to;
    		int lenM = val.length();
    		int lenL = (len - lenM) / 2;
    		int lenR = len - lenM - lenL;
    		val = getSpace(lenL) + val + getSpace(lenR);
    		System.out.println(getSpace(height * len) + val);
    		printInOrder(head.left, height + 1, "^", len);
    	}
    
    	public static String getSpace(int num) {
    		String space = " ";
    		StringBuffer buf = new StringBuffer("");
    		for (int i = 0; i < num; i++) {
    			buf.append(space);
    		}
    		return buf.toString();
    	}
    
    	public static void main(String[] args) {
    
    		Node head = new Node(6);
    		head.left = new Node(1);
    		head.left.left = new Node(0);
    		head.left.right = new Node(3);
    		head.right = new Node(12);
    		head.right.left = new Node(10);
    		head.right.left.left = new Node(4);
    		head.right.left.left.left = new Node(2);
    		head.right.left.left.right = new Node(5);
    		head.right.left.right = new Node(14);
    		head.right.left.right.left = new Node(11);
    		head.right.left.right.right = new Node(15);
    		head.right.right = new Node(13);
    		head.right.right.left = new Node(20);
    		head.right.right.right = new Node(16);
    
    		printTree(head);
    		Node bst = biggestSubBST(head);
    		printTree(bst);
    
    	}
    
    }
    

    morris遍历

    通常,实现二叉树的前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是O(n)的空间复杂度(递归本身占用stack空间或者用户自定义的stack)。

    本文介绍空间O(1)的遍历方法。

    上次文章讲到,我们经典递归遍历其实有三次访问当前节点的机会,就看你再哪次进行操作,而分成了三种遍历。

    https://blog.csdn.net/hebtu666/article/details/82853988

    morris有两次访问节点的机会。

    它省空间的原理是利用了大量叶子节点的没有用的空间,记录之前的节点,做到了返回之前节点这件事情。

    我们不说先序中序后序,先说morris遍历的原则:

    1、如果没有左孩子,继续遍历右子树

    2、如果有左孩子,找到左子树最右节点。

        1)如果最右节点的右指针为空(说明第一次遇到),把它指向当前节点,当前节点向左继续处理。

        2)如果最右节点的右指针不为空(说明它指向之前结点),把右指针设为空,当前节点向右继续处理。

     

    这就是morris遍历。

    请手动模拟深度至少为3的树的morris遍历来熟悉流程。

     

    先看代码:

    定义结点:

    	public static class Node {
    		public int value;
    		Node left;
    		Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}

    先序:

     (完全按规则写就好。)

    //打印时机(第一次遇到):发现左子树最右的孩子右指针指向空,或无左子树。
    	public static void morrisPre(Node head) {
    		if (head == null) {
    			return;
    		}
    		Node cur1 = head;
    		Node cur2 = null;
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					System.out.print(cur1.value + " ");
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    				}
    			} else {
    				System.out.print(cur1.value + " ");
    			}
    			cur1 = cur1.right;
    		}
    		System.out.println();
    	}

    morris在发表文章时只写出了中序遍历。而先序遍历只是打印时机不同而已,所以后人改进出了先序遍历。至于后序,是通过打印所有的右边界来实现的:对每个有边界逆序,打印,再逆序回去。注意要原地逆序,否则我们morris遍历的意义也就没有了。

    完整代码: 

    public class MorrisTraversal {
    
    	
    	
    	public static void process(Node head) {
    		if(head == null) {
    			return;
    		}
    		
    		// 1
    		//System.out.println(head.value);
    		
    		
    		process(head.left);
    		
    		// 2
    		//System.out.println(head.value);
    		
    		
    		process(head.right);
    		
    		// 3
    		//System.out.println(head.value);
    	}
    	
    	
    	public static class Node {
    		public int value;
    		Node left;
    		Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    //打印时机:向右走之前
    	public static void morrisIn(Node head) {
    		if (head == null) {
    			return;
    		}
    		Node cur1 = head;//当前节点
    		Node cur2 = null;//最右
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			//左孩子不为空
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}//找到最右
    				//右指针为空,指向cur1,cur1向左继续
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    				}//右指针不为空,设为空
    			}
    			System.out.print(cur1.value + " ");
    			cur1 = cur1.right;
    		}
    		System.out.println();
    	}
    //打印时机(第一次遇到):发现左子树最右的孩子右指针指向空,或无左子树。
    	public static void morrisPre(Node head) {
    		if (head == null) {
    			return;
    		}
    		Node cur1 = head;
    		Node cur2 = null;
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					System.out.print(cur1.value + " ");
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    				}
    			} else {
    				System.out.print(cur1.value + " ");
    			}
    			cur1 = cur1.right;
    		}
    		System.out.println();
    	}
    //逆序打印所有右边界
    	public static void morrisPos(Node head) {
    		if (head == null) {
    			return;
    		}
    		Node cur1 = head;
    		Node cur2 = null;
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    					printEdge(cur1.left);
    				}
    			}
    			cur1 = cur1.right;
    		}
    		printEdge(head);
    		System.out.println();
    	}
    //逆序打印
    	public static void printEdge(Node head) {
    		Node tail = reverseEdge(head);
    		Node cur = tail;
    		while (cur != null) {
    			System.out.print(cur.value + " ");
    			cur = cur.right;
    		}
    		reverseEdge(tail);
    	}
    //逆序(类似链表逆序)
    	public static Node reverseEdge(Node from) {
    		Node pre = null;
    		Node next = null;
    		while (from != null) {
    			next = from.right;
    			from.right = pre;
    			pre = from;
    			from = next;
    		}
    		return pre;
    	}
    	public static void main(String[] args) {
    		Node head = new Node(4);
    		head.left = new Node(2);
    		head.right = new Node(6);
    		head.left.left = new Node(1);
    		head.left.right = new Node(3);
    		head.right.left = new Node(5);
    		head.right.right = new Node(7);
    
    		morrisIn(head);
    		morrisPre(head);
    		morrisPos(head);
    	}
    
    }

    最小生成树

     

    问题提出:
        要在n个城市间建立通信联络网。顶点:表示城市,权:城市间通信线路的花费代价。希望此通信网花费代价最小。
    问题分析:
        答案只能从生成树中找,因为要做到任何两个城市之间有线路可达,通信网必须是连通的;但对长度最小的要求可以知道网中显然不能有圈,如果有圈,去掉一条边后,并不破坏连通性,但总代价显然减少了,这与总代价最小的假设是矛盾的。
    结论:
        希望找到一棵生成树,它的每条边上的权值之和(即建立该通信网所需花费的总代价)最小 —— 最小代价生成树。
        构造最小生成树的算法很多,其中多数算法都利用了一种称之为 MST 的性质。
        MST 性质:设 N = (V, E)  是一个连通网,U是顶点集 V的一个非空子集。若边 (u, v) 是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边 (u, v) 的最小生成树。


    (1)普里姆 (Prim) 算法

    算法思想: 
        ①设 N=(V, E)是连通网,TE是N上最小生成树中边的集合。
        ②初始令 U={u_0}, (u_0∈V), TE={ }。
        ③在所有u∈U,u∈U-V的边(u,v)∈E中,找一条代价最小的边(u_0,v_0 )。
        ④将(u_0,v_0 )并入集合TE,同时v_0并入U。
        ⑤重复上述操作直至U = V为止,则 T=(V,TE)为N的最小生成树。

     
    代码实现:

    void MiniSpanTree_PRIM(MGraph G,VertexType u)
        //用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。
        //记录从顶点集U到V-U的代价最小的边的辅助数组定义;
        //closedge[j].lowcost表示在集合U中顶点与第j个顶点对应最小权值
    {
        int k, j, i;
        k = LocateVex(G,u);
        for (j = 0; j < G.vexnum; ++j)    //辅助数组的初始化
            if(j != k)
            {
                closedge[j].adjvex = u;
                closedge[j].lowcost = G.arcs[k][j].adj;    
    //获取邻接矩阵第k行所有元素赋给closedge[j!= k].lowcost
            }
        closedge[k].lowcost = 0;        
    //初始,U = {u};  
        PrintClosedge(closedge,G.vexnum);
        for (i = 1; i < G.vexnum; ++i)    \
    //选择其余G.vexnum-1个顶点,因此i从1开始循环
        {
            k = minimum(G.vexnum,closedge);        
    //求出最小生成树的下一个结点:第k顶点
            PrintMiniTree_PRIM(G, closedge, k);     //输出生成树的边
            closedge[k].lowcost = 0;                //第k顶点并入U集
            PrintClosedge(closedge,G.vexnum);
            for(j = 0;j < G.vexnum; ++j)
            {                                           
                if(G.arcs[k][j].adj < closedge[j].lowcost)    
    //比较第k个顶点和第j个顶点权值是否小于closedge[j].lowcost
                {
                    closedge[j].adjvex = G.vexs[k];//替换closedge[j]
                    closedge[j].lowcost = G.arcs[k][j].adj;
                    PrintClosedge(closedge,G.vexnum);
                }
            }
        }
    }


    (2)克鲁斯卡尔 (Kruskal) 算法

    算法思想: 
        ①设连通网  N = (V, E ),令最小生成树初始状态为只有n个顶点而无边的非连通图,T=(V, { }),每个顶点自成一个连通分量。
        ②在 E 中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
    ③依此类推,直至 T 中所有顶点都在同一连通分量上为止。
          
        最小生成树可能不惟一!

     

    拓扑排序

     

    (1)有向无环图

        无环的有向图,简称 DAG (Directed Acycline Graph) 图。
     
    有向无环图在工程计划和管理方面的应用:除最简单的情况之外,几乎所有的工程都可分为若干个称作“活动”的子工程,并且这些子工程之间通常受着一定条件的约束,例如:其中某些子工程必须在另一些子工程完成之后才能开始。
    对整个工程和系统,人们关心的是两方面的问题: 
    ①工程能否顺利进行; 
    ②完成整个工程所必须的最短时间。

    对应到有向图即为进行拓扑排序和求关键路径。 
    AOV网: 
        用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。
    例如:排课表
          
    AOV网的特点:
    ①若从i到j有一条有向路径,则i是j的前驱;j是i的后继。
    ②若< i , j >是网中有向边,则i是j的直接前驱;j是i的直接后继。
    ③AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。


    问题:    
        问题:如何判别 AOV 网中是否存在回路?
        检测 AOV 网中是否存在环方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。


    拓扑排序的方法:
        ①在有向图中选一个没有前驱的顶点且输出之。
        ②从图中删除该顶点和所有以它为尾的弧。
        ③重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止。
            
        一个AOV网的拓扑序列不是唯一的!
    代码实现:

    Status TopologicalSort(ALGraph G)
        //有向图G采用邻接表存储结构。
        //若G无回路,则输出G的顶点的一个拓扑序列并返回OK,否则返回ERROR.
        //输出次序按照栈的后进先出原则,删除顶点,输出遍历
    {
        SqStack S;
        int i, count;
        int *indegree1 = (int *)malloc(sizeof(int) * G.vexnum);
        int indegree[12] = {0};
        FindInDegree(G, indegree);    //求个顶点的入度下标从0开始
        InitStack(&S);
        PrintStack(S);
        for(i = 0; i < G.vexnum; ++i)
            if(!indegree[i])        //建0入度顶点栈S
                push(&S,i);        //入度为0者进栈
        count = 0;                //对输出顶点计数
        while (S.base != S.top)
        {
            ArcNode* p;
            pop(&S,&i);
            VisitFunc(G,i);//第i个输出栈顶元素对应的顶点,也就是最后进来的顶点    
            ++count;          //输出i号顶点并计数
            for(p = G.vertices[i].firstarc; p; p = p->nextarc)
            {    //通过循环遍历第i个顶点的表结点,将表结点中入度都减1
                int k = p->adjvex;    //对i号顶点的每个邻接点的入度减1
                if(!(--indegree[k]))
                    push(&S,k);        //若入度减为0,则入栈
            }//for
        }//while
        if(count < G.vexnum)
        {
            printf("\n该有向图有回路!\n");
            return ERROR;    //该有向图有回路
        }
        else
        {
            printf("\n该有向图没有回路!\n");
            return OK;
        }
    }


    关键路径

        把工程计划表示为有向图,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。称这种有向图为边表示活动的网,简称为 AOE网 (Activity On Edge)。
    例如:
    设一个工程有11项活动,9个事件。
    事件v_1——表示整个工程开始(源点) 
    事件v_9——表示整个工程结束(汇点)

     
    对AOE网,我们关心两个问题:  
    ①完成整项工程至少需要多少时间? 
    ②哪些活动是影响工程进度的关键?
    关键路径——路径长度最长的路径。
    路径长度——路径上各活动持续时间之和。
    v_i——表示事件v_i的最早发生时间。假设开始点是v_1,从v_1到〖v�i〗的最长路径长度。ⅇ(ⅈ)——表示活动a_i的最早发生时间。
    l(ⅈ)——表示活动a_i最迟发生时间。在不推迟整个工程完成的前提下,活动a_i最迟必须开始进行的时间。
    l(ⅈ)-ⅇ(ⅈ)意味着完成活动a_i的时间余量。
    我们把l(ⅈ)=ⅇ(ⅈ)的活动叫做关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程进度。
        例如上图中网,从从v_1到v_9的最长路径是(v_1,v_2,v_5,v_8,ν_9 ),路径长度是18,即ν_9的最迟发生时间是18。而活动a_6的最早开始时间是5,最迟开始时间是8,这意味着:如果a_6推迟3天或者延迟3天完成,都不会影响整个工程的完成。因此,分析关键路径的目的是辨别哪些是关键活动,以便争取提高关键活动的工效,缩短整个工期。
        由上面介绍可知:辨别关键活动是要找l(ⅈ)=ⅇ(ⅈ)的活动。为了求ⅇ(ⅈ)和l(ⅈ),首先应求得事件的最早发生时间vⅇ(j)和最迟发生时间vl(j)。如果活动a_i由弧〈j,k〉表示,其持续时间记为dut(〈j,k〉),则有如下关系:
    ⅇ(ⅈ)= vⅇ(j)
    l(ⅈ)=vl(k)-dut(〈j,k〉)
        求vⅇ(j)和vl(j)需分两步进行:
    第一步:从vⅇ(0)=0开始向前递推
    vⅇ(j)=Max{vⅇ(i)+dut(〈j,k〉)}   〈i,j〉∈T,j=1,2,…,n-1
    其中,T是所有以第j个顶点为头的弧的集合。
    第二步:从vl(n-1)=vⅇ(n-1)起向后递推
    vl(i)=Min{vl(j)-dut(〈i,j〉)}  〈i,j〉∈S,i=n-2,…,0
    其中,S是所有以第i个顶点为尾的弧的集合。
    下面我们以上图AOE网为例,先求每个事件v_i的最早发生时间,再逆向求每个事件对应的最晚发生时间。再求每个活动的最早发生时间和最晚发生时间,如下面表格:
              
    在活动的统计表中,活动的最早发生时间和最晚发生时间相等的,就是关键活动


    关键路径的讨论:

    ①若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。      如:a11、a10、a8、a7。 
    ②如果一个活动处于所有的关键路径上,则提高这个活动的速度,就能缩短整个工程的完成时间。如:a1、a4。
    ③处于所有关键路径上的活动完成时间不能缩短太多,否则会使原关键路径变成非关键路径。这时必须重新寻找关键路径。如:a1由6天变成3天,就会改变关键路径。

    关键路径算法实现:

    int CriticalPath(ALGraph G)
    {    //因为G是有向网,输出G的各项关键活动
        SqStack T;
        int i, j;    ArcNode* p;
        int k , dut;
        if(!TopologicalOrder(G,T))
            return 0;
        int vl[VexNum];
        for (i = 0; i < VexNum; i++)
            vl[i] = ve[VexNum - 1];        //初始化顶点事件的最迟发生时间
        while (T.base != T.top)            //按拓扑逆序求各顶点的vl值
        {
     
            for(pop(&T, &j), p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                k = p->adjvex;    dut = *(p->info);    //dut<j, k>
                if(vl[k] - dut < vl[j])
                    vl[j] = vl[k] - dut;
            }//for
        }//while
        for(j = 0; j < G.vexnum; ++j)    //求ee,el和关键活动
        {
            for (p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                int ee, el;        char tag;
                k = p->adjvex;    dut = *(p->info);
                ee = ve[j];    el = vl[k] - dut;
                tag = (ee == el) ? '*' : ' ';
                PrintCriticalActivity(G,j,k,dut,ee,el,tag);
            }
        }
        return 1;
    }

    最短路

     

    最短路

        典型用途:交通网络的问题——从甲地到乙地之间是否有公路连通?在有多条通路的情况下,哪一条路最短?
     
        交通网络用有向网来表示:顶点——表示城市,弧——表示两个城市有路连通,弧上的权值——表示两城市之间的距离、交通费或途中所花费的时间等。
        如何能够使一个城市到另一个城市的运输时间最短或运费最省?这就是一个求两座城市间的最短路径问题。
        问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n - 1条边。
       常见最短路径问题:单源点最短路径、所有顶点间的最短路径
    (1)如何求得单源点最短路径?
        穷举法:将源点到终点的所有路径都列出来,然后在其中选最短的一条。但是,当路径特别多时,特别麻烦;没有规律可循。
        迪杰斯特拉(Dijkstra)算法:按路径长度递增次序产生各顶点的最短路径。
    路径长度最短的最短路径的特点:
        在此路径上,必定只含一条弧 <v_0, v_1>,且其权值最小。由此,只要在所有从源点出发的弧中查找权值最小者。
    下一条路径长度次短的最短路径的特点:
    ①、直接从源点到v_2<v_0, v_2>(只含一条弧);
    ②、从源点经过顶点v_1,再到达v_2<v_0, v_1>,<v_1, v_2>(由两条弧组成)
    再下一条路径长度次短的最短路径的特点:
        有以下四种情况:
        ①、直接从源点到v_3<v_0, v_3>(由一条弧组成);
        ②、从源点经过顶点v_1,再到达v_3<v_0, v_1>,<v_1, v_3>(由两条弧组成);
        ③、从源点经过顶点v_2,再到达v_3<v_0, v_2>,<v_2, v_3>(由两条弧组成);
        ④、从源点经过顶点v_1  ,v_2,再到达v_3<v_0, v_1>,<v_1, v_2>,<v_2, v_3>(由三条弧组成);
    其余最短路径的特点:    
        ①、直接从源点到v_i<v_0, v_i>(只含一条弧);
        ②、从源点经过已求得的最短路径上的顶点,再到达v_i(含有多条弧)。
    Dijkstra算法步骤:
        初始时令S={v_0},  T={其余顶点}。T中顶点对应的距离值用辅助数组D存放。
        D[i]初值:若<v_0, v_i>存在,则为其权值;否则为∞。 
        从T中选取一个其距离值最小的顶点v_j,加入S。对T中顶点的距离值进行修改:若加进v_j作中间顶点,从v_0到v_i的距离值比不加 vj 的路径要短,则修改此距离值。
        重复上述步骤,直到 S = V 为止。

    算法实现:

    void ShortestPath_DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D)
    { // 用Dijkstra算法求有向网 G 的 v0 顶点到其余顶点v的最短路径P[v]及带权长度D[v]。
        // 若P[v][w]为TRUE,则 w 是从 v0 到 v 当前求得最短路径上的顶点。  P是存放最短路径的矩阵,经过顶点变成TRUE
        // final[v]为TRUE当且仅当 v∈S,即已经求得从v0到v的最短路径。
        int v,w,i,j,min;
        Status final[MAX_VERTEX_NUM];
        for(v = 0 ;v < G.vexnum ;++v)
        {
            final[v] = FALSE;
            D[v] = G.arcs[v0][v].adj;        //将顶点数组中下标对应是 v0 和 v的距离给了D[v]
            for(w = 0;w < G.vexnum; ++w)
                P[v][w] = FALSE;            //设空路径
            if(D[v] < INFINITY)
            {
                P[v][v0] = TRUE;
                P[v][v] = TRUE;
            }
        }
        D[v0]=0;
        final[v0]= TRUE; /* 初始化,v0顶点属于S集 */
        for(i = 1;i < G.vexnum; ++i) /* 其余G.vexnum-1个顶点 */
        { /* 开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集 */
            min = INFINITY; /* 当前所知离v0顶点的最近距离 */
            for(w = 0;w < G.vexnum; ++w)
                if(!final[w]) /* w顶点在V-S中 */
                    if(D[w] < min)
                    {
                        v = w;
                        min = D[w];
                    } /* w顶点离v0顶点更近 */
                    final[v] = TRUE; /* 离v0顶点最近的v加入S集 */
                    for(w = 0;w < G.vexnum; ++w) /* 更新当前最短路径及距离 */
                    {
                        if(!final[w] && min < INFINITY && G.arcs[v][w].adj < INFINITY && (min + G.arcs[v][w].adj < D[w]))
                        { /* 修改D[w]和P[w],w∈V-S */
                            D[w] = min + G.arcs[v][w].adj;
                            for(j = 0;j < G.vexnum;++j)
                                P[w][j] = P[v][j];
                            P[w][w] = TRUE;
                        }
                    }
        }
    }

    简单迷宫问题

    迷宫实验是取自心理学的一个古典实验。在该实验中,把一只老鼠从一个无顶大盒子的门放入,在盒子中设置了许多墙,对行进方向形成了多处阻挡。盒子仅有一个出口,在出口处放置一块奶酪,吸引老鼠在迷宫中寻找道路以到达出口。对同一只老鼠重复进行上述实验,一直到老鼠从入口到出口,而不走错一步。老鼠经过多次试验终于得到它学习走通迷宫的路线。设计一个计算机程序对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论。
    数组元素值为1表示该位置是墙壁,不能通行;元素值为0表示该位置是通路。假定从mg[1][1]出发,出口位于mg[n][m]

    用一种标志在二维数组中标出该条通路,并在屏幕上输出二维数组。

    m=[[1,1,1,0,1,1,1,1,1,1],
       [1,0,0,0,0,0,0,0,1,1],
       [1,0,1,1,1,1,1,0,0,1],
       [1,0,1,0,0,0,0,1,0,1],
       [1,0,1,0,1,1,0,0,0,1],
       [1,0,0,1,1,0,1,0,1,1],
       [1,1,1,1,0,0,0,0,1,1],
       [1,0,0,0,0,1,1,1,0,0],
       [1,0,1,1,0,0,0,0,0,1],
       [1,1,1,1,1,1,1,1,1,1]]
    sta1=0;sta2=3;fsh1=7;fsh2=9;success=0
    def LabyrinthRat():
        print('显示迷宫:')
        for i in range(len(m)):print(m[i])
        print('入口:m[%d][%d]:出口:m[%d][%d]'%(sta1,sta2,fsh1,fsh2))
        if (visit(sta1,sta2))==0:	print('没有找到出口')
        else:
            print('显示路径:')
            for i in range(10):print(m[i])
    def visit(i,j):
        m[i][j]=2
        global success
        if(i==fsh1)and(j==fsh2): success=1
        if(success!=1)and(m[i-1][j]==0): visit(i-1,j)
        if(success!=1)and(m[i+1][j]==0): visit(i+1,j)
        if(success!=1)and(m[i][j-1]==0): visit(i,j-1)
        if(success!=1)and(m[i][j+1]==0): visit(i,j+1)
        if success!=1: m[i][j]=3
        return success
    LabyrinthRat()

    深搜DFS\广搜BFS 

    首先,不管是BFS还是DFS,由于时间和空间的局限性,它们只能解决数据量比较小的问题。

    深搜,顾名思义,它从某个状态开始,不断的转移状态,直到无法转移,然后退回到上一步的状态,继续转移到其他状态,不断重复,直到找到最终的解。从实现上来说,栈结构是后进先出,可以很好的保存上一步状态并利用。所以根据深搜和栈结构的特点,深度优先搜索利用递归函数(栈)来实现,只不过这个栈是系统帮忙做的,不太明显罢了。

     

    广搜和深搜的搜索顺序不同,它是先搜索离初始状态比较近的状态,搜索顺序是这样的:初始状态---------->一步能到的状态--------->两步能到的状态......从实现上说,它是通过队列实现的,并且是我们自己做队列。一般解决最短路问题,因为第一个搜到的一定是最短路。

    下面通过两道简单例题简单的入个门。

    深搜例题

    poj2386

    http://poj.org/problem?id=2386

    题目大意:上下左右斜着挨着都算一个池子,看图中有几个池子。

    W........WW.
    .WWW.....WWW
    ....WW...WW.
    .........WW.
    .........W..
    ..W......W..
    .W.W.....WW.
    W.W.W.....W.
    .W.W......W.
    ..W.......W.例如本图就是有三个池子

    采用深度优先搜索,从任意的w开始,不断把邻接的部分用'.'代替,1次DFS后与初始这个w连接的所有w就全都被替换成'.',因此直到图中不再存在W为止。

    核心代码:

    char field[maxn][maxn];//图
    int n,m;长宽
    void dfs(int x,int y)
    {
        field[x][y]='.';//先做了标记
        //循环遍历八个方向
        for(int dx=-1;dx<=1;dx++){
            for(int dy=-1;dy<=1;dy++){
                int nx=x+dx,ny=y+dy;
                //判断(nx,ny)是否在园子里,以及是否有积水
                if(0<=nx&&nx<n&&0<=ny&&ny<m&&field[nx][ny]=='W'){
                    dfs(nx,ny);
                }
            }
        }
    }
    void solve()
    {
        int res=0;
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(field[i][j]=='W'){
                    //从有积水的地方开始搜
                    dfs(i,j);
                    res++;//搜几次就有几个池子
                }
            }
        }
        printf("%d\n",res);
    }

    广搜例题:

    迷宫的最短路径

      给定一个大小为N×M的迷宫。迷宫由通道和墙壁组成,每一步可以向邻接的上下左右四个的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假定从起点一定可以移动到终点。(N,M≤100)('#', '.' , 'S', 'G'分别表示墙壁、通道、起点和终点)

    输入:

    10 10

    #S######.#
    ......#..#
    .#.##.##.#
    .#........
    ##.##.####
    ....#....#
    .#######.#
    ....#.....
    .####.###.
    ....#...G#

    输出:

    22

    小白书上部分代码:

    typedef pair<int, int> P;
    char maze[maxn][maxn];
    int n, m, sx, sy, gx, gy,d[maxn][maxn];//到各个位置的最短距离的数组
    int dx[4] = { 1,0,-1,0 }, dy[4]= { 0,1,0,-1 };//4个方向移动的向量
    int bfs()//求从(sx,sy)到(gx,gy)的最短距离,若无法到达则是INF
    {
        queue<P> que; 
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                d[i][j] = INF;//所有的位置都初始化为INF
        que.push(P(sx, sy));//将起点加入队列中
        d[sx][sy] = 0;//并把起点的距离设置为0
        while (que.size())//不断循环直到队列的长度为0
        {
            P p = que.front();// 从队列的最前段取出元素
            que.pop();//删除该元素
            if (p.first == gx&&p.second == gy)//是终点结束
                break;
            for (int i = 0; i < 4; i++)//四个方向的循环
            {
                int nx = p.first + dx[i],ny = p.second + dy[i];//移动后的位置标记为(nx,ny)
                if (0 <= nx&&nx < n && 0 <= ny&&ny < m&&maze[nx][ny] != '#'&&d[nx][ny] == INF)//判断是否可以移动以及是否访问过(即d[nx][ny]!=INF)
                {
                    que.push(P(nx, ny));//可以移动,添加到队列
                    d[nx][ny] = d[p.first][p.second] + 1;//到该位置的距离为到p的距离+1
                }
            }
        }
        return d[gx][gy];
    }

    经典了两个题结束了,好题链接持续更新。。。。。。

     皇后问题

     

    八皇后问题是一个以国际象棋为背景的问题:如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n1×n1,而皇后个数也变成n2。而且仅当 n2 ≥ 1 或 n1 ≥ 4 时问题有解。

    皇后问题是非常著名的问题,作为一个棋盘类问题,毫无疑问,用暴力搜索的方法来做是一定可以得到正确答案的,但在有限的运行时间内,我们很难写出速度可以忍受的搜索,部分棋盘问题的最优解不是搜索,而是动态规划,某些棋盘问题也很适合作为状态压缩思想的解释例题。

    进一步说,皇后问题可以用人工智能相关算法和遗传算法求解,可以用多线程技术缩短运行时间。本文不做讨论。

    (本文不展开讲状态压缩,以后再说)

     

    一般思路:

     

    N*N的二维数组,在每一个位置进行尝试,在当前位置上判断是否满足放置皇后的条件(这一点的行、列、对角线上,没有皇后)。

     

    优化1:

     

    既然知道多个皇后不能在同一行,我们何必要在同一行的不同位置放多个来尝试呢?

    我们生成一维数组record,record[i]表示第i行的皇后放在了第几列。对于每一行,确定当前record值即可,因为每行只能且必须放一个皇后,放了一个就无需继续尝试。那么对于当前的record[i],查看record[0...i-1]的值,是否有j = record[k](同列)、|record[k] - j| = | k-i |(同一斜线)的情况。由于我们的策略,无需检查行(每行只放一个)。

    public class NQueens {
    	public static int num1(int n) {
    		if (n < 1) {
    			return 0;
    		}
    		int[] record = new int[n];
    		return process1(0, record, n);
    	}
    	public static int process1(int i, int[] record, int n) {
    		if (i == n) {
    			return 1;
    		}
    		int res = 0;
    		for (int j = 0; j < n; j++) {
    			if (isValid(record, i, j)) {
    				record[i] = j;
    				res += process1(i + 1, record, n);
    			}
    		}//对于当前行,依次尝试每列
    		return res;
    	}
    //判断当前位置是否可以放置
    	public static boolean isValid(int[] record, int i, int j) {
    		for (int k = 0; k < i; k++) {
    			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
    				return false;
    			}
    		}
    		return true;
    	}
    	public static void main(String[] args) {
    		int n = 8;
    		System.out.println(num1(n));
    	}
    }
    

    优化2:

     

    分析:棋子对后续过程的影响范围:本行、本列、左右斜线。

    黑色棋子影响区域为红色

    本行影响不提,根据优化一已经避免

    本列影响,一直影响D列,直到第一行在D放棋子的所有情况结束。

     

    左斜线:每向下一行,实际上对当前行的影响区域就向左移动

    比如:

    尝试第二行时,黑色棋子影响的是我们的第三列;

    尝试第三行时,黑色棋子影响的是我们的第二列;

    尝试第四行时,黑色棋子影响的是我们的第一列;

    尝试第五行及以后几行,黑色棋子对我们并无影响。

     

    右斜线则相反:

    随着行序号增加,影响的列序号也增加,直到影响的列序号大于8就不再影响。

     

    我们对于之前棋子影响的区域,可以用二进制数字来表示,比如:

    每一位,用01代表是否影响。

    比如上图,对于第一行,就是00010000

    尝试第二行时,数字变为00100000

    第三行:01000000

    第四行:10000000

     

    对于右斜线的数字,同理:

    第一行00010000,之后向右移:00001000,00000100,00000010,00000001,直到全0不影响。

     

    同理,我们对于多行数据,也同样可以记录了

    比如在第一行我们放在了第四列:

    第二行放在了G列,这时左斜线记录为00100000(第一个棋子的影响)+00000010(当前棋子的影响)=00100010。

    到第三行数字继续左移:01000100,然后继续加上我们的选择,如此反复。

     

    这样,我们对于当前位置的判断,其实可以通过左斜线变量、右斜线变量、列变量,按位或运算求出(每一位中,三个数有一个是1就不能再放)。

    具体看代码:

    注:怎么排版就炸了呢。。。贴一张图吧

    public class NQueens {
    	public static int num2(int n) {
    		// 因为本方法中位运算的载体是int型变量,所以该方法只能算1~32皇后问题
    		// 如果想计算更多的皇后问题,需使用包含更多位的变量
    		if (n < 1 || n > 32) {
    			return 0;
    		}
    		int upperLim = n == 32 ? -1 : (1 << n) - 1;
            //upperLim的作用为棋盘大小,比如8皇后为00000000 00000000 00000000 11111111
            //32皇后为11111111 11111111 11111111 11111111
    		return process2(upperLim, 0, 0, 0);
    	}
    
    	public static int process2(int upperLim, int colLim, int leftDiaLim,
    			int rightDiaLim) {
    		if (colLim == upperLim) {
    			return 1;
    		}
    		int pos = 0;            //pos:所有的合法位置
    		int mostRightOne = 0;   //所有合法位置的最右位置
    
            //所有记录按位或之后取反,并与全1按位与,得出所有合法位置
    		pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
    		int res = 0;//计数
    		while (pos != 0) {
    			mostRightOne = pos & (~pos + 1);//取最右的合法位置
    			pos = pos - mostRightOne;       //去掉本位置并尝试
    			res += process2(
                         upperLim,                             //全局
                         colLim | mostRightOne,                //列记录
                         //之前列+本位置
    					(leftDiaLim | mostRightOne) << 1,      //左斜线记录
                         //(左斜线变量+本位置)左移             
    					(rightDiaLim | mostRightOne) >>> 1);   //右斜线记录
                         //(右斜线变量+本位置)右移(高位补零)
    		}
    		return res;
    	}
    
    	public static void main(String[] args) {
    		int n = 8;
    		System.out.println(num2(n));
    	}
    }
    

    完整测试代码:

    32皇后:结果/时间

    暴力搜:时间就太长了,懒得测。。。

    public class NQueens {
    
    	public static int num1(int n) {
    		if (n < 1) {
    			return 0;
    		}
    		int[] record = new int[n];
    		return process1(0, record, n);
    	}
    
    	public static int process1(int i, int[] record, int n) {
    		if (i == n) {
    			return 1;
    		}
    		int res = 0;
    		for (int j = 0; j < n; j++) {
    			if (isValid(record, i, j)) {
    				record[i] = j;
    				res += process1(i + 1, record, n);
    			}
    		}
    		return res;
    	}
    
    	public static boolean isValid(int[] record, int i, int j) {
    		for (int k = 0; k < i; k++) {
    			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
    				return false;
    			}
    		}
    		return true;
    	}
    
    	public static int num2(int n) {
    		if (n < 1 || n > 32) {
    			return 0;
    		}
    		int upperLim = n == 32 ? -1 : (1 << n) - 1;
    		return process2(upperLim, 0, 0, 0);
    	}
    
    	public static int process2(int upperLim, int colLim, int leftDiaLim,
    			int rightDiaLim) {
    		if (colLim == upperLim) {
    			return 1;
    		}
    		int pos = 0;
    		int mostRightOne = 0;
    		pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
    		int res = 0;
    		while (pos != 0) {
    			mostRightOne = pos & (~pos + 1);
    			pos = pos - mostRightOne;
    			res += process2(upperLim, colLim | mostRightOne,
    					(leftDiaLim | mostRightOne) << 1,
    					(rightDiaLim | mostRightOne) >>> 1);
    		}
    		return res;
    	}
    
    	public static void main(String[] args) {
    		int n = 32;
    
    		long start = System.currentTimeMillis();
    		System.out.println(num2(n));
    		long end = System.currentTimeMillis();
    		System.out.println("cost time: " + (end - start) + "ms");
    
    		start = System.currentTimeMillis();
    		System.out.println(num1(n));
    		end = System.currentTimeMillis();
    		System.out.println("cost time: " + (end - start) + "ms");
    	}
    }
    

    二叉搜索树实现

    本文给出二叉搜索树介绍和实现

     

    首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

     

    那这个结构有什么有用呢?

    首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

    基本操作:

    1、插入某个数值

    2、查询是否包含某个数值

    3、删除某个数值

     

    根据实现不同,还可以实现其他很多种操作。

     

    实现思路思路:

    前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

    而删除稍微复杂一些,有下面这几种情况:

    1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

    2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

    3、以上都不满足,就把左儿子子孙中最大节点提上来。

     

    当然,反过来也是成立的,比如右儿子子孙中最小的节点。

     

    下面来叙述为什么可以这么做。

    下图中A为待删除节点。

    第一种情况:

     

    1、去掉A,把c提上来,c也是小于x的没问题。

    2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

     

    第二种情况

     

    3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

    4、同理

     

    第三种情况

     

    5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

    因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

    而黑点小于A,也就小于c,所以可以让c当右孩子

    大概证明就这样。。

    下面我们用代码实现并通过注释理解

    上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

    定义

    struct node
    {
        int val;//数据
        node *lch,*rch;//左右孩子
    };

    插入

     node *insert(node *p,int x)
     {
         if(p==NULL)//直到空就创建节点
         {
             node *q=new node;
             q->val=x;
             q->lch=q->rch=NULL;
             return p;
         }
         if(x<p->val)p->lch=insert(p->lch,x);
         else p->lch=insert(p->rch,x);
         return p;//依次返回自己,让上一个函数执行。
     }

    查找

     bool find(node *p,int x)
     {
         if(p==NULL)return false;
         else if(x==p->val)return true;
         else if(x<p->val)return find(p->lch,x);
         else return find(p->rch,x);
     }

    删除

     node *remove(node *p,int x)
     {
          if(p==NULL)return NULL;
          else if(x<p->val)p->lch=remove(p->lch,x);
          else if(x>p->val)p->lch=remove(p->rch,x);
          //以下为找到了之后
          else if(p->lch==NULL)//情况1
          {
              node *q=p->rch;
              delete p;
              return q;
          }
          else if(p->lch->rch)//情况2
          {
              node *q=p->lch;
              q->rch=p->rch;
              delete p;
              return q;
          }
          else
          {
              node *q;
              for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个
              node *r=q->rch;//最大节点
              q->rch=r->lch;//最大节点左孩子提到最大节点位置
              r->lch=p->lch;//调整黑点左孩子为B
              r->rch=p->rch;//调整黑点右孩子为c
              delete p;//删除
              return r;//返回给父
          }
          return p;
     }

    Abstract Self-Balancing Binary Search Tree

     

    二叉搜索树

     

    二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
    具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/81741034

    我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,

    此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度

     

    概念引入

     

    Abstract Self-Balancing Binary Search Tree:自平衡二叉搜索树

    顾名思义:它在面对任意节点插入和删除时自动保持其高度

    常用算法有红黑树、AVL、Treap、伸展树、SB树等。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在O(log(n)),大大降低了操作的时间复杂度。这些结构为可变有序列表提供了有效的实现,并且可以用于其他抽象数据结构,例如关联数组优先级队列集合

    对于这些结构,他们都有自己的平衡性,比如:

    AVL树

    具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

    根据定义可知,这是根据深度最严苛的标准了,左右子树高度不能差的超过1.

    具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/85047648

     

    红黑树

    特性:
    (1)每个节点或者是黑色,或者是红色。
    (2)根节点是黑色。
    (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
    (4)如果一个节点是红色的,则它的子节点必须是黑色的。
    (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

    根据定义,确保没有一条路径会比其他路径长出2倍。

     

    size balance tree

    Size Balanced Tree(简称SBT)是一自平衡二叉查找树,是在计算机科学中用到的一种数据结构。它是由中国广东中山纪念中学的陈启峰发明的。陈启峰于2006年底完成论文《Size Balanced Tree》,并在2007年的全国青少年信息学奥林匹克竞赛冬令营中发表。由于SBT的拼写很容易找到中文谐音,它常被中国的信息学竞赛选手和ACM/ICPC选手们戏称为“傻B树”、“Super BT”等。相比红黑树、AVL树等自平衡二叉查找树,SBT更易于实现。据陈启峰在论文中称,SBT是“目前为止速度最快的高级二叉搜索树”。SBT能在O(log n)的时间内完成所有二叉搜索树(BST)的相关操作,而与普通二叉搜索树相比,SBT仅仅加入了简洁的核心操作Maintain。由于SBT赖以保持平衡的是size域而不是其他“无用”的域,它可以很方便地实现动态顺序统计中的selectrank操作。

    对于SBT的每一个结点 t,有如下性质:
       性质(a) s[ right[t] ]≥s[ left [ left[ t ] ] ], s[ right [ left[t] ] ]
       性质(b) s[ left[t] ]≥s[right[ right[t] ] ], s[ left[ right[t] ] ]
    即.每棵子树的大小不小于其兄弟的子树大小。

     

    伸展树

    伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由Daniel Sleator和Robert Tarjan创造。它的优势在于不需要记录用于平衡树的冗余信息。在伸展树上的一般操作都基于伸展操作。

     

    Treap

    Treap是一棵二叉排序树,它的左子树和右子树分别是一个Treap,和一般的二叉排序树不同的是,Treap纪录一个额外的数据,就是优先级。Treap在以关键码构成二叉排序树的同时,还满足的性质(在这里我们假设节点的优先级大于该节点的孩子的优先级)。但是这里要注意的是Treap二叉堆有一点不同,就是二叉堆必须是完全二叉树,而Treap并不一定是。

     

     

     

     

    对比可以发现,AVL树对平衡性的要求比较严苛,每插入一个节点就很大概率面临调整。

    而红黑树对平衡性的要求没有那么严苛。可能是多次插入攒够了一下调整。。。

     

    把每一个树的细节都扣清楚是一件挺无聊的事。。虽然据说红黑树都成了面试必问内容,但是实在是不想深究那些细节,这些树的基本操作也无非是那么两种:左旋,右旋。这些树的所有操作和情况,都是这两种动作的组合罢了。

    所以本文先介绍这两种基本操作,等以后有时间(可能到找工作时),再把红黑树等结构的细节补上。

     

    最简单的旋转

     

    最简单的例子:

    这棵树,左子树深度为2,右子树深度为0,所以,根据AVL树或者红黑树的标准,它都不平衡。。

    那怎么办?转过来:

    是不是就平衡了?

    这就是我们的顺时针旋转,又叫,右旋,因为是以2为轴,把1转下来了。

    左旋同理。

     

    带子树旋转

    问题是,真正转起来可没有这么简单:

    这才是一颗搜索树的样子啊

    ABCD都代表是一颗子树。我们这三个点转了可不能不管这些子树啊对不对。

    好,我们想想这些子树怎么办。

    首先,AB子树没有关系,放在原地即可。

    D作为3的右子树,也可以不动,那剩下一个位置,会不会就是放C子树呢?

    我们想想能否这样做。

    原来:

    1)C作为2的右子树,内任何元素都比2大。

    2)C作为3左子树的一部分,内任何元素都比3小。

    转之后:

    1)C作为2的右子树的一部分,内任何元素都比2大。

    2)C作为3左子树,内任何元素都比3小。

    所以,C子树可以作为3的左子树,没有问题。

    这样,我们的操作就介绍完了。

    这种基本的变换达到了看似把树变的平衡的效果。

    左右旋转类似

     

    代码实现

    对于Abstract BinarySearchTree类,上面网址已经给出了思路和c++代码实现,把java再贴出来也挺无趣的,所以希望大家能自己实现。

    抽象自平衡二叉搜索树(AbstractSelfBalancingBinarySearchTree)的所有操作都是建立在二叉搜索树(BinarySearchTree )操作的基础上来进行的。

    各种自平衡二叉搜索树(AVL、红黑树等)的操作也是由Abstract自平衡二叉搜索树的基本操作:左旋、右旋构成。这个文章只写了左旋右旋基本操作,供以后各种selfBalancingBinarySearchTree使用。

    public abstract class AbstractSelfBalancingBinarySearchTree extends AbstractBinarySearchTree {
        protected Node rotateRight(Node node) {
            Node temp = node.left;//节点2
            temp.parent = node.parent;
            //节点3的父(旋转后节点2的父)
            node.left = temp.right;
            //节点3接收节点2的右子树
            if (node.left != null) {
                node.left.parent = node;
            }
    
            temp.right = node;
            //节点3变为节点2的右孩子
            node.parent = temp;
    
            //原来节点3的父(若存在),孩子变为节点2
            if (temp.parent != null) {
                if (node == temp.parent.left) {
                    temp.parent.left = temp;
                } else {
                    temp.parent.right = temp;
                }
            } else {
                root = temp;
            }
            return temp;
        }
    
        protected Node rotateLeft(Node node) {
            Node temp = node.right;
            temp.parent = node.parent;
            node.right = temp.left;
            if (node.right != null) {
                node.right.parent = node;
            }
            temp.left = node;
            node.parent = temp;
            if (temp.parent != null) {
                if (node == temp.parent.left) {
                    temp.parent.left = temp;
                } else {
                    temp.parent.right = temp;
                }
            } else {
                root = temp;
            }
            
            return temp;
        }
    }

     

    AVL Tree

     

    前言

     

    希望读者

    了解二叉搜索树

    了解左旋右旋基本操作

    https://blog.csdn.net/hebtu666/article/details/84992363

    直观感受直接到文章底部,有正确的调整策略动画,自行操作。

    二叉搜索树
     

    二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
    具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/81741034

    我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,

    此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。
     

    AVL Tree

    计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

    这种结构是对平衡性要求最严苛的self-Balancing Binary Search Tree。

    旋转操作继承自self-Balancing Binary Search Tree

    public class AVLTree extends AbstractSelfBalancingBinarySearchTree

    旋转

    上面网址中已经介绍了二叉搜索树的调整和自平衡二叉搜索树的基本操作(左旋右旋),上篇文章我是这样定义左旋的:

    达到了   看似   更平衡的效果。

    我们回忆一下:

    看起来好像不是很平,对吗?我们转一下:

    看起来平了很多。

    但!是!

    只是看起来而已。

    我们知道。ABCD其实都是子树,他们也有自己的深度,如果是这种情况:

    我们简化一下:

    转之后(A上来,3作为A的右孩子,A的右子树作为新的3的左孩子):

    没错,旋转确实让树变平衡了,这是因为,不平衡是由A的左子树造成的,A的左子树深度更深。

    我们这样旋转实际上是让

    A的左子树相对于B提上去了两层,深度相对于B,-2,

    A的右子树相对于B提上去了一层,深度相对于B,-1.

    而如果是这样的:

    旋转以后:

    依旧是不平的。

    那我们怎么解决这个问题呢?

    先3的左子树旋转:

    细节问题:不再讲解

    这样,我们的最深处又成了左子树的左子树。然后再按原来旋转就好了。

     

    旋转总结

     

    那我们来总结一下旋转策略:

    单向右旋平衡处理LL:

    由于在*a的左子树根结点的左子树上插入结点,*a的平衡因子由1增至2,致使以*a为根的子树失去平衡,则需进行一次右旋转操作;

    单向左旋平衡处理RR:

    由于在*a的右子树根结点的右子树上插入结点,*a的平衡因子由-1变为-2,致使以*a为根的子树失去平衡,则需进行一次左旋转操作;

    双向旋转(先左后右)平衡处理LR:

    由于在*a的左子树根结点的右子树上插入结点,*a的平衡因子由1增至2,致使以*a为根的子树失去平衡,则需进行两次旋转(先左旋后右旋)操作。

    双向旋转(先右后左)平衡处理RL:

    由于在*a的右子树根结点的左子树上插入结点,*a的平衡因子由-1变为-2,致使以*a为根的子树失去平衡,则需进行两次旋转(先右旋后左旋)操作。

     

    深度的记录

     

    我们解决了调整问题,但是我们怎么发现树不平衡呢?总不能没插入删除一次都遍历一下求深度吧。

    当然要记录一下了。

    我们需要知道左子树深度和右子树深度。这样,我们可以添加两个变量,记录左右子树的深度。

    但其实不需要,只要记录自己的深度即可。然后左右子树深度就去左右孩子去寻找即可。

    这样就引出了一个问题:深度的修改、更新策略是什么呢?

    单个节点的深度更新

    本棵树的深度=(左子树深度,右子树深度)+1

    所以写出节点node的深度更新方法:

        private static final void updateHeight(AVLNode node) {
    //不存在孩子,为-1,最后+1,深度为0
            int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height;
            int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height;
            node.height = 1 + Math.max(leftHeight, rightHeight);
        }

     

    写出旋转代码

    配合上面的方法和文章头部给出文章Abstract Self-Balancing Binary Search Tree的旋转,我们可以AVL树的四种旋转:

        private Node avlRotateLeft(Node node) {
            Node temp = super.rotateLeft(node);
            updateHeight((AVLNode)temp.left);
            updateHeight((AVLNode)temp);
            return temp;
        }
    
        private Node avlRotateRight(Node node) {
            Node temp = super.rotateRight(node);
            updateHeight((AVLNode)temp.right);
            updateHeight((AVLNode)temp);
            return temp;
        }
    
        protected Node doubleRotateRightLeft(Node node) {
            node.right = avlRotateRight(node.right);
            return avlRotateLeft(node);
        }
    
        protected Node doubleRotateLeftRight(Node node) {
            node.left = avlRotateLeft(node.left);
            return avlRotateRight(node);
        }

    请自行模拟哪些节点的深度记录需要修改。

     

    总写调整方法

     

    我们写出了旋转的操作和相应的深度更新。

    现在我们把这些方法分情况总写。

        private void rebalance(AVLNode node) {
            while (node != null) {
                Node parent = node.parent;
                
                int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height;
                int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height;
                int nodeBalance = rightHeight - leftHeight;
    
                if (nodeBalance == 2) {
                    if (((AVLNode)node.right.right).height+1 == rightHeight) {
                        node = (AVLNode)avlRotateLeft(node);
                        break;
                    } else {
                        node = (AVLNode)doubleRotateRightLeft(node);
                        break;
                    }
                } else if (nodeBalance == -2) {
                    if (((AVLNode)node.left.left).height+1 == leftHeight) {
                        node = (AVLNode)avlRotateRight(node);
                        break;
                    } else {
                        node = (AVLNode)doubleRotateLeftRight(node);
                        break;
                    }
                } else {
                    updateHeight(node);//平衡就一直往上更新高度
                }
                
                node = (AVLNode)parent;
            }
        }

    插入完工

     

    我们的插入就完工了。

        public Node insert(int element) {
            Node newNode = super.insert(element);//插入
            rebalance((AVLNode)newNode);//调整
            return newNode;
        }

     

    删除

    也是一样的思路,自底向上,先一路修改高度后,进行rebalance调整。

        public Node delete(int element) {
            Node deleteNode = super.search(element);
            if (deleteNode != null) {
                Node successorNode = super.delete(deleteNode);
                //结合上面网址二叉搜索树实现的情况介绍
                if (successorNode != null) {
                    // if replaced from getMinimum(deleteNode.right) 
                    // then come back there and update heights
                    AVLNode minimum = successorNode.right != null ? (AVLNode)getMinimum(successorNode.right) : (AVLNode)successorNode;
                    recomputeHeight(minimum);
                    rebalance((AVLNode)minimum);
                } else {
                    recomputeHeight((AVLNode)deleteNode.parent);//先修改
                    rebalance((AVLNode)deleteNode.parent);//再调整
                }
                return successorNode;
            }
            return null;
        }
        /**
         * Recomputes height information from the node and up for all of parents. It needs to be done after delete.
         */
        private void recomputeHeight(AVLNode node) {
           while (node != null) {
              node.height = maxHeight((AVLNode)node.left, (AVLNode)node.right) + 1;
              node = (AVLNode)node.parent;
           }
        }
        
        /**
         * Returns higher height of 2 nodes. 
         */
        private int maxHeight(AVLNode node1, AVLNode node2) {
            if (node1 != null && node2 != null) {
                return node1.height > node2.height ? node1.height : node2.height;
            } else if (node1 == null) {
                return node2 != null ? node2.height : -1;
            } else if (node2 == null) {
                return node1 != null ? node1.height : -1;
            }
            return -1;
        }

    请手动模拟哪里的高度需要改,哪里不需要改。

     

    直观表现程序

     

    如果看的比较晕,或者直接从头跳下来的同学,这个程序是正确的模拟了,维护AVL树的策略和一些我没写的基本操作。大家可以自己操作,直观感受一下。

    https://www.cs.usfca.edu/~galles/visualization/AVLtree.html?utm_source=qq&utm_medium=social&utm_oi=826801573962338304

     

    跳表介绍和实现

     

    想慢慢的给大家自然的引入跳表。

     

    想想,我们

    1)在有序数列里搜索一个数

    2)或者把一个数插入到正确的位置

    都怎么做?

    很简单吧

    对于第一个操作,我们可以一个一个比较,在数组中我们可以二分,这样比链表快

    对于第二个操作,二分也没什么用,因为找到位置还要在数组中一个一个挪位置,时间复杂度依旧是o(n)。

    那我们怎么发明一个查找插入都比较快的结构呢?

     

     

     

    可以打一些标记:

    这样我们把标记连起来,搜索一个数时先从标记开始搜起下一个标记比本身大的话就往下走,因为再往前就肯定不符合要求了。

    比如我们要搜索18:

    因为一次可以跨越好多数呀,自然快了一些。

    既然可以打标记,我们可以改进一下,选出一些数来再打一层标记:

    这样我们搜索20是这样的:

    最终我们可以打好多层标记,我们从最高层开始搜索,一次可以跳过大量的数(依旧是右边大了就往下走)。

    比如搜索26:

    最好的情况,就是每一层的标记都减少一半,这样到了顶层往下搜索,其实和二分就没什么两样,我们最底层用链表串起来,插入一个元素也不需要移动元素,所谓跳表就完成了一大半了。

     

    现在的问题是,我们对于一个新数,到底应该给它打几层标记呢?

    (刚开始一个数都没有,所以解决了这个问题,我们一直用这个策略更新即可)

    答案是。。。。。投硬币,全看脸。

    我其实有点惊讶,我以为会有某些很强的和数学相关的算法,可以保证一个很好的搜索效率,是我想多了。

    我们对于一个新数字,有一半概率可以打一层标记,有一半概率不可以打。

    对于打了一层标记的数,我们依旧是这个方法,它有一半概率再向上打一层标记,依次循环。

    所以每一层能到达的概率都少一半。

    各层的节点数量竟然就可以比较好的维护在很好的效率上(最完美的就是达到了二分的效果)

     

    再分析一下,其实对于同一个数字:

    等等。。

    其实没必要全都用指针,因为我们知道,通过指针找到一个数可比下标慢多了。

    所以同一个数字的所有标记,没必要再用指针,效率低还不好维护,用一个list保存即可。

    这样,我们就设计出来一个数字的所有标记组成的结构:

    	public static class SkipListNode {
    		public Integer value;//本身的值
    		public ArrayList<SkipListNode> nextNodes;
    //指向下一个元素的结点组成的数组,长度全看脸。
    
    		public SkipListNode(Integer value) {
    			this.value = value;
    			nextNodes = new ArrayList<SkipListNode>();
    		}
    	}

    将integer比较的操作封装一下:

    		private boolean lessThan(Integer a, Integer b) {
    			return a.compareTo(b) < 0;
    		}
    
    		private boolean equalTo(Integer a, Integer b) {
    			return a.compareTo(b) == 0;
    		}

    找到在本层应该往下拐的结点:

    		// Returns the node at a given level with highest value less than e
    		private SkipListNode findNext(Integer e, SkipListNode current, int level) {
    			SkipListNode next = current.nextNodes.get(level);
    			while (next != null) {
    				Integer value = next.value;
    				if (lessThan(e, value)) { // e < value
    					break;
    				}
    				current = next;
    				next = current.nextNodes.get(level);
    			}
    			return current;
    		}

    这样我们就写一个一层层往下找的方法,并且封装成find(Integer e)的形式:

    		// Returns the skiplist node with greatest value <= e
    		private SkipListNode find(Integer e) {
    			return find(e, head, maxLevel);
    		}
    
    		// Returns the skiplist node with greatest value <= e
    		// Starts at node start and level
    		private SkipListNode find(Integer e, SkipListNode current, int level) {
    			do {
    				current = findNext(e, current, level);
    			} while (level-- > 0);
    			return current;
    		}

    刚才的方法是找到最大的小于等于目标的值,如果找到的值等于目标,跳表中就存在这个目标。否则不存在。

    		public boolean contains(Integer value) {
    			SkipListNode node = find(value);
    			return node != null && node.value != null && equalTo(node.value, value);
    		}

    我们现在可以实现加入一个新点了,要注意把每层的标记打好:

    		public void add(Integer newValue) {
    			if (!contains(newValue)) {
    				size++;
    				int level = 0;
    				while (Math.random() < PROBABILITY) {
    					level++;//能有几层全看脸
    				}
    				while (level > maxLevel) {//大于当前最大层数
    					head.nextNodes.add(null);//直接连系统最大
    					maxLevel++;
    				}
    				SkipListNode newNode = new SkipListNode(newValue);
    				SkipListNode current = head;//前一个结点,也就是说目标应插current之后
    				do {//每一层往下走之前就可以设置这一层的标记了,就是链表插入一个新节点
    					current = findNext(newValue, current, level);
    					newNode.nextNodes.add(0, current.nextNodes.get(level));
    					current.nextNodes.set(level, newNode);
    				} while (level-- > 0);
    			}
    		}

    删除也是一样的

    		public void delete(Integer deleteValue) {
    			if (contains(deleteValue)) {
    				SkipListNode deleteNode = find(deleteValue);
    				size--;
    				int level = maxLevel;
    				SkipListNode current = head;
    				do {//就是一个链表删除节点的操作
    					current = findNext(deleteNode.value, current, level);
    					if (deleteNode.nextNodes.size() > level) {
    						current.nextNodes.set(level, deleteNode.nextNodes.get(level));
    					}
    				} while (level-- > 0);
    			}
    		}

    作为一个容器,Iterator那是必须有的吧,里面肯定有hasNext和next吧?

    	public static class SkipListIterator implements Iterator<Integer> {
    		SkipList list;
    		SkipListNode current;
    
    		public SkipListIterator(SkipList list) {
    			this.list = list;
    			this.current = list.getHead();
    		}
    
    		public boolean hasNext() {
    			return current.nextNodes.get(0) != null;
    		}
    
    		public Integer next() {
    			current = current.nextNodes.get(0);
    			return current.value;
    		}
    	}

    这个跳表我们就实现完了。

    现实工作中呢,我们一般不会让它到无限多层,万一有一个数它人气爆炸随机数冲到了一万层呢?

    所以包括redis在内的一些跳表实现,都是规定了一个最大层数的。

    别的好像也没什么了。

    最后贴出所有代码。

    import java.util.ArrayList;
    import java.util.Iterator;
    
    public SkipListDemo {
    
    	public static class SkipListNode {
    		public Integer value;
    		public ArrayList<SkipListNode> nextNodes;
    
    		public SkipListNode(Integer value) {
    			this.value = value;
    			nextNodes = new ArrayList<SkipListNode>();
    		}
    	}
    
    	public static class SkipListIterator implements Iterator<Integer> {
    		SkipList list;
    		SkipListNode current;
    
    		public SkipListIterator(SkipList list) {
    			this.list = list;
    			this.current = list.getHead();
    		}
    
    		public boolean hasNext() {
    			return current.nextNodes.get(0) != null;
    		}
    
    		public Integer next() {
    			current = current.nextNodes.get(0);
    			return current.value;
    		}
    	}
    
    	public static class SkipList {
    		private SkipListNode head;
    		private int maxLevel;
    		private int size;
    		private static final double PROBABILITY = 0.5;
    
    		public SkipList() {
    			size = 0;
    			maxLevel = 0;
    			head = new SkipListNode(null);
    			head.nextNodes.add(null);
    		}
    
    		public SkipListNode getHead() {
    			return head;
    		}
    
    		public void add(Integer newValue) {
    			if (!contains(newValue)) {
    				size++;
    				int level = 0;
    				while (Math.random() < PROBABILITY) {
    					level++;
    				}
    				while (level > maxLevel) {
    					head.nextNodes.add(null);
    					maxLevel++;
    				}
    				SkipListNode newNode = new SkipListNode(newValue);
    				SkipListNode current = head;
    				do {
    					current = findNext(newValue, current, level);
    					newNode.nextNodes.add(0, current.nextNodes.get(level));
    					current.nextNodes.set(level, newNode);
    				} while (level-- > 0);
    			}
    		}
    
    		public void delete(Integer deleteValue) {
    			if (contains(deleteValue)) {
    				SkipListNode deleteNode = find(deleteValue);
    				size--;
    				int level = maxLevel;
    				SkipListNode current = head;
    				do {
    					current = findNext(deleteNode.value, current, level);
    					if (deleteNode.nextNodes.size() > level) {
    						current.nextNodes.set(level, deleteNode.nextNodes.get(level));
    					}
    				} while (level-- > 0);
    			}
    		}
    
    		// Returns the skiplist node with greatest value <= e
    		private SkipListNode find(Integer e) {
    			return find(e, head, maxLevel);
    		}
    
    		// Returns the skiplist node with greatest value <= e
    		// Starts at node start and level
    		private SkipListNode find(Integer e, SkipListNode current, int level) {
    			do {
    				current = findNext(e, current, level);
    			} while (level-- > 0);
    			return current;
    		}
    
    		// Returns the node at a given level with highest value less than e
    		private SkipListNode findNext(Integer e, SkipListNode current, int level) {
    			SkipListNode next = current.nextNodes.get(level);
    			while (next != null) {
    				Integer value = next.value;
    				if (lessThan(e, value)) { // e < value
    					break;
    				}
    				current = next;
    				next = current.nextNodes.get(level);
    			}
    			return current;
    		}
    
    		public int size() {
    			return size;
    		}
    
    		public boolean contains(Integer value) {
    			SkipListNode node = find(value);
    			return node != null && node.value != null && equalTo(node.value, value);
    		}
    
    		public Iterator<Integer> iterator() {
    			return new SkipListIterator(this);
    		}
    
    		/******************************************************************************
    		 * Utility Functions *
    		 ******************************************************************************/
    
    		private boolean lessThan(Integer a, Integer b) {
    			return a.compareTo(b) < 0;
    		}
    
    		private boolean equalTo(Integer a, Integer b) {
    			return a.compareTo(b) == 0;
    		}
    
    	}
    
    	public static void main(String[] args) {
    
    	}
    
    }
    
    

    c语言实现排序和查找所有算法

     

     c语言版排序查找完成,带详细解释,一下看到爽,能直接运行看效果。

     

    /* Note:Your choice is C IDE */
    #include "stdio.h"
    #include"stdlib.h"
    #define MAX 10
    void SequenceSearch(int *fp,int Length);
    void Search(int *fp,int length);
    void Sort(int *fp,int length);
    /*
    注意:
        1、数组名x,*(x+i)就是x[i]哦
    
    */
    
    
    /*
    ================================================
    功能:选择排序
    输入:数组名称(数组首地址)、数组中元素个数
    ================================================
    */
    void select_sort(int *x, int n)
    {
        int i, j, min, t;
        for (i=0; i<n-1; i++) /*要选择的次数:下标:0~n-2,共n-1次*/
        {
            min = i; /*假设当前下标为i的数最小,比较后再调整*/
            for (j=i+1; j<n; j++)/*循环找出最小的数的下标是哪个*/
            {
                if (*(x+j) < *(x+min))
                    min = j; /*如果后面的数比前面的小,则记下它的下标*/
            }
            if (min != i) /*如果min在循环中改变了,就需要交换数据*/
            {
                t = *(x+i);
                *(x+i) = *(x+min);
                *(x+min) = t;
            }
        }
    }
    /*
    ================================================
    功能:直接插入排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    ================================================
    */
    
    void insert_sort(int *x, int n)
    {
        int i, j, t;
        for (i=1; i<n; i++) /*要选择的次数:下标1~n-1,共n-1次*/
        {
            /*
             暂存下标为i的数。注意:下标从1开始,原因就是开始时
             第一个数即下标为0的数,前面没有任何数,认为它是排
             好顺序的。
            */
            t=*(x+i);
            for (j=i-1; j>=0 && t<*(x+j); j--) /*注意:j=i-1,j--,这里就是下标为i的数,在它前面有序列中找插入位置。*/
            {
                *(x+j+1) = *(x+j); /*如果满足条件就往后挪。最坏的情况就是t比下标为0的数都小,它要放在最前面,j==-1,退出循环*/
            }
            *(x+j+1) = t; /*找到下标为i的数的放置位置*/
        }
    }
    /*
    ================================================
    功能:冒泡排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    ================================================
    */
    void bubble_sort0(int *x, int n)
    {
        int j, h, t;
        for (h=0; h<n-1; h++)/*循环n-1次*/
        {
            for (j=0; j<n-2-h; j++)/*每次做的操作类似*/
            {
                if (*(x+j) > *(x+j+1)) /*大的放在后面,小的放到前面*/
                {
                    t = *(x+j);
                    *(x+j) = *(x+j+1);
                    *(x+j+1) = t; /*完成交换*/
                }
            }
        }
    }
    /*优化:记录最后下沉位置,之后的肯定有序*/
    void bubble_sort(int *x, int n)
    {
        int j, k, h, t;
        for (h=n-1; h>0; h=k) /*循环到没有比较范围*/
        {
            for (j=0, k=0; j<h; j++) /*每次预置k=0,循环扫描后更新k*/
            {
                if (*(x+j) > *(x+j+1)) /*大的放在后面,小的放到前面*/
                {
                    t = *(x+j);
                    *(x+j) = *(x+j+1);
                    *(x+j+1) = t; /*完成交换*/
                    k = j; /*保存最后下沉的位置。这样k后面的都是排序排好了的。*/
                }
            }
        }
    }
    /*
    ================================================
    功能:希尔排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    ================================================
    */
    
    void shell_sort(int *x, int n)
    {
        int h, j, k, t;
        for (h=n/2; h>0; h=h/2) /*控制增量*/
        {
            for (j=h; j<n; j++) /*这个实际上就是上面的直接插入排序*/
            {
                t = *(x+j);
                for (k=j-h; (k>=0 && t<*(x+k)); k-=h)
                {
                    *(x+k+h) = *(x+k);
                }
                *(x+k+h) = t;
            }
        }
    }
    /*
    ================================================
    功能:快速排序
    输入:数组名称(也就是数组首地址)、数组中起止元素的下标
    注:自己画画
    ================================================
    */
    
    void quick_sort(int *x, int low, int high)
    {
        int i, j, t;
        if (low < high) /*要排序的元素起止下标,保证小的放在左边,大的放在右边。这里以下标为low的元素(最左边)为基准点*/
        {
            i = low;
            j = high;
            t = *(x+low); /*暂存基准点的数*/
            while (i<j) /*循环扫描*/
            {
                while (i<j && *(x+j)>t) /*在右边的只要比基准点大仍放在右边*/
                {
                    j--; /*前移一个位置*/
                }
                if (i<j)
                {
                    *(x+i) = *(x+j); /*上面的循环退出:即出现比基准点小的数,替换基准点的数*/
                    i++; /*后移一个位置,并以此为基准点*/
                }
                while (i<j && *(x+i)<=t) /*在左边的只要小于等于基准点仍放在左边*/
                {
                    i++; /*后移一个位置*/
                }
                if (i<j)
                {
                    *(x+j) = *(x+i); /*上面的循环退出:即出现比基准点大的数,放到右边*/
                    j--; /*前移一个位置*/
                }
            }
            *(x+i) = t; /*一遍扫描完后,放到适当位置*/
            quick_sort(x,low,i-1);   /*对基准点左边的数再执行快速排序*/
            quick_sort(x,i+1,high);   /*对基准点右边的数再执行快速排序*/
        }
    }
    /*
    ================================================
    功能:堆排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    注:画画
    ================================================
    */
    /*
    功能:建堆
    输入:数组名称(也就是数组首地址)、参与建堆元素的个数、从第几个元素开始
    */
    void sift(int *x, int n, int s)
    {
        int t, k, j;
        t = *(x+s); /*暂存开始元素*/
        k = s;   /*开始元素下标*/
        j = 2*k + 1; /*左子树元素下标*/
        while (j<n)
        {
            if (j<n-1 && *(x+j) < *(x+j+1))/*判断是否存在右孩子,并且右孩子比左孩子大,成立,就把j换为右孩子*/
            {
                j++;
            }
            if (t<*(x+j)) /*调整*/
            {
                *(x+k) = *(x+j);
                k = j; /*调整后,开始元素也随之调整*/
                j = 2*k + 1;
            }
            else /*没有需要调整了,已经是个堆了,退出循环。*/
            {
                break;
            }
        }
        *(x+k) = t; /*开始元素放到它正确位置*/
    }
    /*
    功能:堆排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    注:
                *
             *     *
           *   -  *   *
          * * * 
    建堆时,从从后往前第一个非叶子节点开始调整,也就是“-”符号的位置
    */
    void heap_sort(int *x, int n)
    {
        int i, k, t;
    //int *p;
        for (i=n/2-1; i>=0; i--)
        {
            sift(x,n,i); /*初始建堆*/
        }
        for (k=n-1; k>=1; k--)
        {
            t = *(x+0); /*堆顶放到最后*/
            *(x+0) = *(x+k);
            *(x+k) = t;
            sift(x,k,0); /*剩下的数再建堆*/
        }
    }
    
    
    
    
    // 归并排序中的合并算法
    void Merge(int a[], int start, int mid, int end)
    {
        int i,k,j, temp1[10], temp2[10];
        int n1, n2;
        n1 = mid - start + 1;
        n2 = end - mid;
    
        // 拷贝前半部分数组
        for ( i = 0; i < n1; i++)
        {
            temp1[i] = a[start + i];
        }
        // 拷贝后半部分数组
        for (i = 0; i < n2; i++)
        {
            temp2[i] = a[mid + i + 1];
        }
        // 把后面的元素设置的很大
        temp1[n1] = temp2[n2] = 1000;
        // 合并temp1和temp2
        for ( k = start, i = 0, j = 0; k <= end; k++)
        {
            //小的放到有顺序的数组里
            if (temp1[i] <= temp2[j])
            {
                a[k] = temp1[i];
                i++;
            }
            else
            {
                a[k] = temp2[j];
                j++;
            }
        }
    }
    
    // 归并排序
    void MergeSort(int a[], int start, int end)
    {
        if (start < end)
        {
            int i;
            i = (end + start) / 2;
            // 对前半部分进行排序
            MergeSort(a, start, i);
            // 对后半部分进行排序
            MergeSort(a, i + 1, end);
            // 合并前后两部分
            Merge(a, start, i, end);
        }
    }
    /*顺序查找*/
    void SequenceSearch(int *fp,int Length)
    {
        int i;
        int data;
        printf("开始使用顺序查询.\n请输入你想要查找的数据.\n");
        scanf("%d",&data);
        for(i=0; i<Length; i++)
            if(fp[i]==data)
            {
                printf("经过%d次查找,查找到数据%d,表中位置为%d.\n",i+1,data,i);
                return ;
            }
        printf("经过%d次查找,未能查找到数据%d.\n",i,data);
    }
    /*二分查找*/
    void Search(int *fp,int Length)
    {
        int data;
        int bottom,top,middle;
        int i=0;
        printf("开始使用二分查询.\n请输入你想要查找的数据.\n");
        scanf("%d",&data);
        printf("由于二分查找法要求数据是有序的,现在开始为数组排序.\n");
        Sort(fp,Length);
        printf("数组现在已经是从小到大排列,下面将开始查找.\n");
        bottom=0;
        top=Length;
        while (bottom<=top)
        {
            middle=(bottom+top)/2;
            i++;
            if(fp[middle]<data)
            {
                bottom=middle+1;
            }
            else if(fp[middle]>data)
            {
                top=middle-1;
            }
            else
            {
                printf("经过%d次查找,查找到数据%d,在排序后的表中的位置为%d.\n",i,data,middle);
                return;
            }
        }
        printf("经过%d次查找,未能查找到数据%d.\n",i,data);
    }
    
    /*
    
    下面测试了
    
    */
    void Sort(int *fp,int Length)
    {
        int temp;
        int i,j,k;
        printf("现在开始为数组排序,排列结果将是从小到大.\n");
        for(i=0; i<Length; i++)
            for(j=0; j<Length-i-1; j++)
                if(fp[j]>fp[j+1])
                {
                    temp=fp[j];
                    fp[j]=fp[j+1];
                    fp[j+1]=temp;
                }
        printf("排序完成!\n下面输出排序后的数组:\n");
        for(k=0; k<Length; k++)
        {
            printf("%5d",fp[k]);
        }
        printf("\n");
    
    }
    /*构造随机输出函数类*/
    void input(int a[])
    {
        int i;
        srand( (unsigned int)time(NULL) );
        for (i = 0; i < 10; i++)
        {
            a[i] = rand() % 100;
        }
        printf("\n");
    }
    /*构造键盘输入函数类*/
    /*void input(int *p)
    {
         int i;
         printf("请输入 %d 个数据 :\n",MAX);
          for (i=0; i<MAX; i++)
          {
           scanf("%d",p++);
          }
          printf("\n");
    }*/
    /*构造输出函数类*/
    void output(int *p)
    {
        int i;
        for ( i=0; i<MAX; i++)
        {
            printf("%d ",*p++);
        }
    }
    void main()
    {
        int start=0,end=3;
        int *p, i, a[MAX];
        int count=MAX;
        int arr[MAX];
        int choise=0;
        /*printf("请输入你的数据的个数:\n");
        scanf("%d",&count);*/
        /* printf("请输入%d个数据\n",count);
        for(i=0;i<count;i++)
        {
           scanf("%d",&arr[i]);
        }*/
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        do
        {
            printf("1.使用顺序查询.\n2.使用二分查找法查找.\n3.退出\n");
            scanf("%d",&choise);
            if(choise==1)
                SequenceSearch(a,count);
            else if(choise==2)
                Search(a,count);
            else if(choise==3)
                break;
        }
        while (choise==1||choise==2||choise==3);
    
    
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试选择排序*/
        p = a;
        printf("选择排序之后的数据:\n");
        select_sort(p,MAX);
        output(a);
        printf("\n");
        system("pause");
        /**/
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试直接插入排序*/
        printf("直接插入排序之后的数据:\n");
        p = a;
        insert_sort(p,MAX);
        output(a);
        printf("\n");
        system("pause");
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试冒泡排序*/
        printf("冒泡排序之后的数据:\n");
        p = a;
        insert_sort(p,MAX);
        output(a);
        printf("\n");
        system("pause");
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试快速排序*/
        printf("快速排序之后的数据:\n");
        p = a;
        quick_sort(p,0,MAX-1);
        output(a);
        printf("\n");
        system("pause");
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试堆排序*/
        printf("堆排序之后的数据:\n");
        p = a;
        heap_sort(p,MAX);
        output(a);
        printf("\n");
        system("pause");
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试归并排序*/
        printf("归并排序之后的数据:\n");
        p = a;
        MergeSort(a,start,end);
        output(a);
        printf("\n");
        system("pause");
    }

     

     

    展开全文
  • 超硬核!数据结构学霸笔记,考试面试吹牛就靠它

    万次阅读 多人点赞 2021-03-26 11:11:21
    上次发操作系统笔记,很快浏览上万,这次数据结构比上次硬核的多哦,同样的会发超硬核代码,关注吧。

    上次发操作系统笔记,很快浏览上万,这次数据结构比上次硬核的多哦,同样的会发超硬核代码,关注吧。

    超硬核!操作系统学霸笔记,考试复习面试全靠它

     


     


     

    第一次笔记(复习c,课程概述)

    第一节课复习了c语言的一些知识,并简单介绍了数据结构这门课程。

     

    1、引用和函数调用:

    1.1引用:对一个数据建立一个“引用”,他的作用是为一个变量起一个别名。这是C++对C语言的一个重要补充。

    用法很简单:

    int a = 5;

    int &b = a;

    b是a别名,b与a代表的是同一个变量,占内存中同一个存储单元,具有同一地址。

    注意事项:

    1. 声明一个引用,同时必须初始化,及声明它代表哪一个变量。(作为函数参数时不需要初始化)
       
    2. 在声明一个引用后,不能再作为另一变量的引用。

         3。不能建立引用数组。

    1.2函数调用:

    其实还是通过函数来理解了一下引用

    void Myswap1(int a,int b)
    {
    	int c = a;
    	a = b;
    	b = c;
    }
    
    void Myswap2(int &a,int &b)
    {
    	int c = a;
    	a = b;
    	b = c;
    }
    
    void Myswap3(int *pa,int *pb)
    {
    	int c = *pa;
    	*pa = *pb;
    	*pb = c;
    }

    这三个函数很简单,第一个只传了值进来,不改变全局变量;而第三个也很熟悉,就是传递了地址,方便操作。依旧是“值传递”的方式,只不过传递的是变量的地址而已;那二就彻底改变了这些东西,引用作为函数参数,传入的实参就是变量,而不是数值,真正意义上的“变量传递”。

     

    2、数组和指针:

    这一块讲得比较简单,就是基本知识。

    主要内容:

    1、函数传数组就是传了个指针,这个大家都知道,所以传的时候你写arr[],里面写多少,或者不写,都是没关系的,那你后面一定要放一个变量来把数组长度传进来。

    2、还有就是,定义:int arr[5],你访问越界是不会报错的,但是逻辑上肯定就没有道理了。那用typedef int INTARR[3];访问越界,在vs上会报错,要注意。

    3、再说一下指针和数组名字到底有什么区别?这本来就是两个东西,可能大家没注意罢了。

    第一:指针可以自增,数组名不行,因为是常量啊。

    第二:地址不同,虽然名字[n],都可以这样用,但是数组名地址就是第一个元素地址。指针地址就是那个指针的地址,指针里存的才是第一个元素地址。

    第三:sizeof(),空间不一样,数组是占数组那么大空间。指针是四个字节。

    本来就是俩东西,这么多不同都是本质不同的体现。

    3、结构体:

    也是讲的基本操作,基本就是这个东西:

    typedef struct Date
    {
    	int Year;
    	int Month;
    	int Day;
    	struct Date *pDate;
    }Date, *pDate;

    1、内部无指向自己的指针才可以第一行先不起名字。

    2、内部不能定义自己的,如果能的话那空间不就无限了么。很简单的逻辑

     

    指针我不习惯,还是写Date *比较顺眼

    3、有同学没注意:访问结构体里的东西怎么访问?

    Date.这种形式,或者有指向这个节点的指针p可以p->这种形式,别写错了。

     

    4、还有就是结构体能直接这么赋值:

       Date d1 = {2018,9,11};

    我竟然不知道,以前还傻乎乎的一个一个赋值呢。

     

    5、还有,想写一下结构体有什么优点。。

    这一块可能写的就不好了,因为不是老师讲的。。

    比如学生成绩,如果不用结构体,我们一个学生可能有十几个信息,那定义变量和操作就很烦琐了,数据结构有一种松散的感觉。用一个结构体来表示更好,无论是程序的可读性还是可移植性还是可维护性,都得到提升。还有就是函数调用的时候,传入相当多的参数,又想操作或者返回,那是很麻烦的事。现在只传入一个结构体就好了,操作极其简单。总结一下就是好操作,中看中用,有机地组织了对象的属性。以修改结构体成员变量的方法代替了函数(入口参数)的重新定义。

    基本就这样吧。

    6、还有就是它了:typedef int INTARR[3];这样直接定义了一个数据类型,长度为3的数组,然后直接这样用就可以了:

    INTARR arr1;

     

    回忆完C语言储备知识,然后讲了数据结构的基本概念

     

    数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。

    数据:是对客观事物的符号表示,在计算机中指能输入到计算机中并被处理的符号总称。

    数据元素:数据的基本单位

    数据项:数据的不可分割的最小单位

    数据对象:性质相同的数据元素的集合。

    举例:动物是数据,某只动物是数据元素,猫狗是数据对象,颜色可以是数据项。

     

    数据元素之间存在某种关系,这种关系成为结构。

    四种基本结构:

    集合:除了同属一个集合无其他关系。

    线性结构:一对一的关系

    树形结构:一对多的关系

    图状结构:多对多的关系

     

    第二次笔记(基本概念,时间空间复杂度)

    今天继续说明了一些基本概念,讲解了时间空间复杂度。

    (对于概念的掌握也很重要)

     

    元素之间的关系在计算机中有两种表示方法:顺序映像和非顺序映像,由此得到两种不同的储存结构:

    顺序存储结构和链式存储结构。

     

    顺序:根据元素在存储器中的相对位置表示关系

    链式:借助指针表示关系

     

    数据类型:是一个值的集合和定义在这个值集上的一组操作的总称。

    抽象数据类型:是指一个数学模型以及定义在该模型上的一组操作。(仅仅取决于逻辑特性,与其在计算机内部如何表示和实现无关)

     

    定义抽象数据类型的一种格式:

    ADT name{

    数据对象:<>

    数据关系:<>

    基本操作:<>

    }ADT name

     

    算法:是对特定问题求解步骤的一种描述。

    算法五个特性:

    1. 有穷性:有穷的时间内完成,或者可以说是可接受的时间完成
    2. 确定性:对于相同的输入只能得到相同的输出
    3. 可行性:描述的操作都可以执行基本操作有限次来实现
    4. 输入:零个或多个输入。取自于某个特定对象的集合
    5. 输出:一个或多个输出

     

    设计要求:正确性、可读性、健壮性、效率与低存储量需求。

    执行频度概念:是指通过统计算法的每一条指令的执行次数得到的。

    执行频度=算法中每一条语句执行次数的和

    一般认定每条语句执行一次所需时间为单位时间(常数时间)O(1)

     

    几个小知识和小问题:

    1)循环执行次数n+1次,不是n次。第一次执行i=1和判断i<=n以后执行n次判断和i++。所以该语句执行了n+1次。在他的控制下,循环体执行了n次。

    2)四个并列程序段:分别为O(N),O(N^2),O(N^3),O(N*logN),整个程序时间复杂度为O(N^3),因为随着N的增长,其它项都会忽略

    3)算法分析的目的是分析算法的效率以求改进

    4)对算法分析的前提是算法必须正确

    5)原地工作指的不是不需要空间,而是空间复杂度O(1),可能会需要有限几个变量。

    实现统一功能两种算法:时间O(2^N),O(N^10),假设计算机可以运行10^7秒,每秒可执行基本操作10^5次,问可解问题规模各为多少?选哪个更为合适?

    计算机一共可执行10^7*10^5=10^12次

    第一种:n=log2,(10^12)=12log(2,10)

    第二种:n=(10^12)^0.1

    显然1更适用。

    虽然一般情况多项式算法比指数阶更优

     

    时间空间复杂度概述

     

    找个时间写一写时间复杂度和一些问题分类,也普及一下这方面知识。

    如何衡量一个算法好坏

    很显然,最重要的两个指标:需要多久可以解决问题、解决问题耗费了多少资源

    那我们首先说第一个问题,要多长时间来解决某个问题。那我们可以在电脑上真实的测试一下嘛,多种方法比一比,用时最少的就是最优的啦。

    但是没必要,我们可以通过分析计算来确定一个方法的好坏,用O()表示,括号内填入N、1,等式子。

    这到底是什么意思呢?

    简单来说,就是这个方法,时间随着数据规模变化而增加的快慢。时间可以当成Y,数据规模是X,y=f(x),就这样而已。但是f(x)不是准确的,只是一个大致关系,y=10x,我们也视作x,因为他的增长速度还是n级别的。现在就可以理解了:一般O(N)就是对每个对象访问优先次数而已。请注意:O(1)它不是每个元素访问一次,而是Y=1的感觉,y不随x变化而变化,数据多大它的时间是不变的,有限的常数操作即可完成。

    那我们就引入正规概念:

    时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。

    计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。

    注意:文中提到:不包括这个函数的低阶项和首项系数。什么意思呢?就是说10n,100n,哪怕1000000000n,还是算做O(N),而低阶项是什么意思?不知大家有没有学高等数学1,里面有最高阶无穷大,就是这个意思。举个例子。比如y=n*n*n+n*n+n+1000

    就算做o(n*n*n),因为增长速率最大,N*N及其它项增长速率慢,是低阶无穷大,n无限大时,忽略不计。

     

    那接着写:o(n*n*n)的算法一定不如o(n)的算法吗?也不一定,因为之前说了,时间复杂度忽略了系数,什么意思?o(n)可以是10000000n,当n很小的时候,前者明显占优。

    所以算法要视实际情况而定。

    算法的时间 复杂度常见的有:
    常数阶 O(1),对数阶 O(log n),线性阶 O(n),
    线性对数阶 O(nlog n),平方阶 O(n^2),立方阶 O(n^3),…,
    k 次方阶O(n^k),指数阶 O(2^n),阶乘阶 O(n!)。

    常见的算法的时间 复杂度之间的关系为:
           O(1)<O(log n)<O(n)<O(nlog n)<O(n^2)<O(2^n)<O(n!)<O(n^n) 

     

    我们在竞赛当中,看见一道题,第一件事就应该是根据数据量估计时间复杂度。

    计算机计算速度可以视作10^9,如果数据量是10000,你的算法是O(N*N),那就很玄,10000*10000=10000 0000,别忘了还有常数项,这种算法只有操作比较简单才可能通过。你可以想一想O(nlog n)的算法一般就比较稳了。那数据量1000,一般O(N*N)就差不多了,数据量更小就可以用复杂度更高的算法。大概就这样估算。

     

    当 n 很大时,指数阶算法和多项式阶算法在所需时间上非常
    悬殊。因此,只要有人能将现有指数阶算法中的任何一个算法化
    简为多项式阶算法,那就取得了一个伟大的成就。

    体会一下:

     

    空间复杂度也是一样,用来描述占空间的多少。

    注意时间空间都不能炸。

    所以才发明了那么多算法。

    符上排序算法的时间空间表,体会一下:

     


    排序博客:加深对时间空间复杂度理解

    https://blog.csdn.net/hebtu666/article/details/81434236

     

     

     

    引入:算法优化

     

    想写一系列文章,总结一些题目,看看解决问题、优化方法的过程到底是什么样子的。

     

    系列问题一:斐波那契数列问题

    在数学上,斐波纳契数列以如下被以递归的方法定义:F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)根据定义,前十项为1, 1, 2, 3, 5, 8, 13, 21, 34, 55

    问题一:

    给定一个正整数n,求出斐波那契数列第n项(这时n较小)

    解法一:最笨,效率最低,暴力递归,完全是抄定义。请看代码

    def f0(n):
        if n==1 or n==2:
            return 1
        return f(n-1)+f(n-2)

     

    分析一下,为什么说递归效率很低呢?咱们来试着运行一下就知道了

     

    比如想求f(10),计算机里怎么运行的?

     

    每次要计算的函数量都是指数型增长,时间复杂度O(2^(N/2))<=T(N)<=O(2^N),效率很低。效率低的原因是,进行了大量重复计算,比如图中的f(8),f(7).....等等,你会发现f(8)其实早就算过了,但是你后来又要算一遍。

    如果我们把计算的结果全都保存下来,按照一定的顺序推出n项,就可以提升效率, 斐波那契(所有的一维)的顺序已经很明显了,就是依次往下求。。

    优化1

    def f1(n):
        if n==1 or n==2:
            return 1
        l=[0]*n
        l[0],l[1]=1,1
        for i in range(2,n):
            l[i]=l[i-1]+l[i-2]
        return l[-1]

     

    时间复杂度o(n),依次往下打表就行,空间o(n).

    继续优化:既然只求第n项,而斐波那契又严格依赖前两项,那我们何必记录那么多值呢?记录前两项就行了

     

    优化2

    def f2(n):
        a,b=1,1
        for i in range(n-1):
            a,b=b,a+b
        return a

    此次优化做到了时间o(n),空间o(1)

    附:这道题掌握到这里就可以了,但是其实有时间o(log2n)的方法

     

    优化三:

    学习过线性代数的同学们能够理解:

     

    结合快速幂算法,我们可以在o(log2n)内求出某个对象的n次幂。(在其它专题详细讲解)

    注意:只有递归式不变,才可以用矩阵+快速幂的方法。

    注:优化三可能只有在面试上或竞赛中用,当然,快速幂还是要掌握的。

     

    经过这个最简单的斐波那契,大家有没有一点感觉了?

    好,我们继续往下走

    (补充:pat、蓝桥杯原题,让求到一万多位,结果模一个数。

    可以每个结果都对这个数取模,否则爆内存,另:对于有多组输入并且所求结果类似的题,可以先求出所有结果存起来,然后直接接受每一个元素,在表中查找相应答案)

     

    问题二:

    一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    依旧是找递推关系,分析:跳一阶,就一种方法,跳两阶,它可以一次跳两个,也可以一个一个跳,所以有两种,三个及三个以上,假设为n阶,青蛙可以是跳一阶来到这里,或者跳两阶来到这里,只有这两种方法。它跳一阶来到这里,说明它上一次跳到n-1阶,同理,它也可以从n-2跳过来,f(n)为跳到n的方法数,所以,f(n)=f(n-1)+f(n-2)

    和上题的优化二类似。

    因为递推式没变过,所以可以用优化三

     

    问题三:

    我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?提示,大矩形看做是长度吧

    请读者自己先思考一下吧。。。仔细看题。。仔细思考

    如果n是1,只有一种,竖着放呗;n是2,两种:

    n等于3,三种:

     

    题意应该理解了吧?

    读到这里,你们应该能很快想到,依旧是斐波那契式递归啊。

    对于n>=3:怎么能覆盖到三?只有两种办法,从n-1的地方竖着放了一块,或者从n-2的位置横着放了两块呗。

    和问题二的代码都不用变。

     

    问题四:

    给定一个由0-9组成的字符串,1可以转化成A,2可以转化成B。依此类推。。25可以转化成Y,26可以转化成z,给一个字符串,返回能转化的字母串的有几种?

    比如:123,可以转化成

    1 2 3变成ABC,

    12 3变成LC,

    1 23变成AW,三种,返回三,

    99999,就一种:iiiii,返回一。

    分析:求i位置及之前字符能转化多少种。

    两种转化方法,一,字符i自己转换成自己对应的字母,二,和前面那个数组成两位数,然后转换成对应的字母

    假设遍历到i位置,判断i-1位置和i位置组成的两位数是否大于26,大于就没有第二种方法,f(i)=f(i-1),想反,等于f(i-1)+f(i-2)

    注意:递归式不确定,不能用矩阵快速幂

     

    (这几个题放到这里就是为了锻炼找递归关系的能力,不要只会那个烂大街的斐波那契)

     

    '''
    斐波那契问题:
    斐波纳契数列以如下被以递归的方法定义:
    F(1)=1
    F(2)=1
    F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
    '''
    #暴力抄定义,过多重复计算
    def f0(n):
        if n==1 or n==2:
            return 1
        return f(n-1)+f(n-2)
    #记录结果后依次递推的优化,时间O(N)
    def f1(n):
        if n==1 or n==2:
            return 1
        l=[0]*n
        l[0],l[1]=1,1
        for i in range(2,n):
            l[i]=l[i-1]+l[i-2]
        return l[-1]
    #既然严格依赖前两项,不必记录每一个结果,优化空间到O(1)
    def f2(n):
        a,b=1,1
        for i in range(n-1):
            a,b=b,a+b
        return a

    第三次笔记(线性表结构和顺序表示)

     

    这节课介绍了线性表结构和顺序表示的一部分内容。

    操作太多,而且书上有,就不一一介绍分析了。

    线性表定义:n个数据元素的有限序列。

    特点:

    1. 存在唯一一个称作“第一个”的元素。
    2. 存在唯一一个称作“最后一个”的元素
    3. 除最后一个元素外,集合中每一个元素都只有一个直接前趋
    4. 除最后一个元素外,集合中每一个元素都只有一个直接后继

    注意1、2条:证明循环的序列不是线性表

     

    注意:

    1)线性表从1开始,线性表第一个元素对应到数组中下标是0.

    2)函数通过引用对线性表的元素进行修改即可

    3)数组比较特别,它即可视为逻辑结构,又可视为存储结构。

    4)每一个表元素都是不可再分的原子数据。一维数组可以视为线性表,二维数组不可以,在逻辑上它最多可以有两个直接前趋和直接后继。

    5)线性表具有逻辑上的顺序性,在序列中各元素有其先后次序,各个数据元素在线性表中的逻辑位置只取决于序号。

     

    顺序表:是线性表的循序储存结构,以物理位置表示逻辑关系,任意元素可以随机存取。用一组地址连续的存储单元依次存储线性表中的元素。逻辑顺序和物理顺序是一致的。可以顺序访问,也可随机访问。

    顺序表存储:

    这些定式还是很重要的,比如define typedef等,真正实现时最好就这样写,不要自己规定个长度和数据类型,这样以后好维护、修改。

    静态存储分配:

    #define maxSize 100//显式定义表长

    Typedef int DataType;//定义数据类型

    Typedef struct{

    DataType data[maxSize];//静态分配存储表元素向量

    Int n;//实际表中个数

    }SeqList;

     

    动态存储分配:

    #define maxSize 100//长度初始定义

    Typedef int DataType;//定义数据类型

    Typedef struct{

    DataType *data;//动态分配数组指针

    Int maxSize,n;//最大容量和当前个数

    }SeqList;

     

    初始动态分配:

    Data=(DataType *)malloc(sizeof(DataType)* initSize);

    C++:data=new DataType[initSize];

    maxSize=initSize;n=0;

    动态分配存储,向量的存储空间是在程序执行过程中通过动态存储分配来获取的。空间满了就另外分配一块新的更大的空间,用来代替原来的存储空间,从而达到扩充的目的。

     

    插入:需要查找,移动元素,概率上1,2,3....n,平均O(N)

    删除:同样需要移动元素。填充被空出来的存储单元。

    在等概率下平均移动次数分别为n/2,(n-1)/2

     插入注意事项:

    1. 需要判断是否已满
    2. 要从后向前移动,否则会冲掉元素

    删除注意事项:

    1. 需要先判断是否已空
    2. 需要把后方元素前移,要从前向后。

     

    注意:线性表的顺序存储借用了一维数组,但是二者不同:

    1. 一维数组各非空结点可以不相继存放,但顺序表是相继存放的
    2. 顺序表长度是可变的,一维数组长度是确定的,一旦分配就不可变
    3. 一维数组只能按下标存取元素,顺序表可以有线性表的所有操作。

     

     

    第四次笔记(链表概述)

     

    介绍了链表和基本操作

    用一组物理位置任意的存储单元来存放线性表的数据元素。 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。因此,链表中元素的逻辑次序和 物理次序不一定相同。

     

    定义:

    typedef  struct  Lnode{  
            //声明结点的类型和指向结点的指针类型  
            ElemType         data;    //数据元素的类型 
            struct   Lnode  *next;   //指示结点地址的指针  
    }Lnode, *LinkList;  
    struct Student
    { char num[8];   //数据域
      char name[8];  //数据域
      int score;          //数据域
      struct Student *next;  // next 既是 struct Student 
                                           // 类型中的一个成员,又指 
                                           // 向 struct Student 类型的数据。 
    }Stu_1, Stu_2, Stu_3, *LL;  
    

    头结点:在单链表的第一个结点之前人为地附设的一个结点。

    带头结点操作会方便很多。

    带和不带的我都写过了

    下面列出我见过的一些好题

    1、

    线性表的每个结点只能是一个简单类型,而链表的每个结点可以是一个复杂类型。

    • 正确
    • 错误

     

    错,线性表是逻辑结构概念,可以顺序存储或链式储,与元素数据类型无关。链表就是线性表的一种  前后矛盾

     

    2、

    若某线性表中最常用的操作是在最后一个元素之后插入一个元素和删除第一个元素,则采用(    )存储方式最节省运算时间。

    • 单链表
    • 仅有头指针的单循环链表
    • 双链表
    • 仅有尾指针的单循环链表

      对于A,B,C要想在尾端插入结点,需要遍历整个链表。

      对于D,要插入结点,只要改变一下指针即可,要删除头结点,只要删除指针.next的元素即可。

     

    3、注意:线性表是具有n个数据元素的有限序列,而不是数据项

     

    4、

    以下关于单向链表说法正确的是

    • 如果两个单向链表相交,那他们的尾结点一定相同
    • 快慢指针是判断一个单向链表有没有环的一种方法
    • 有环的单向链表跟无环的单向链表不可能相交
    • 如果两个单向链表相交,那这两个链表都一定不存在环

    自己多画画想想就明白了,关于快慢指针我以后会写总结。

     

    5、

    链接线性表是顺序存取的线性表 。 ( )

    • 正确
    • 错误

    链接线性表可以理解为链表
    线性表分为顺序表和链表
    顺序表是顺序存储结构、随机存取结构
    链表是随机存储结构、顺序存取结构

     

    6、

    typedef struct node_s{
        int item;
        struct node_s* next;
    }node_t;
    node_t* reverse_list(node_t* head)
    {
        node_t* n=head;
        head=NULL;
        while(n){
        _________               
        }
        return head;
     }

    空白处填入以下哪项能实现该函数的功能?

    • node_t* m=head; head=n; head->next=m; n=n->next;
    • node_t* m=n; n=n->next; m->next=head; head=m;
    • node_t* m=n->next; n->next=head; n=m; head=n;
    • head=n->next; head->next=n; n=n->next;


     

    代码的功能是要实现链表的反转。为了方便阐述,每个结点用①②③④⑤⑥等来标示。

    在执行while(n)循环之前,有两句代码:

    node_t* n=head;

    head=NULL;

    这两行代码的中:第一句的作用是用一个临时变量n来保存结点①,第二句是把head修改为NULL。

    然后就开始遍历了,我们看到while循环里的那四句代码:

    node_t* m=n; 
    n=n->next; 
    m->next=head; 
    head=m;
    

    先看前两句,用m来保存n,然后让n指向n的下一个结点,之所以复制 n 给 m ,是因为 n 的作用其实是  控制while循环次数  的作用,每循环一次它就要被修改为指向下一个结点。

    再看后两句,变量head在这里像是一个临时变量,后两句让 m 指向了 head,然后 head 等于 m。

     

    7、

    若某表最常用的操作是在最后一个结点之后插入一个节点或删除最后一二个结点,则采用()省运算时间。

    • 单链表
    • 双链表
    • 单循环链表
    • 带头结点的双循环链表

    D

    带头结点的双向循环链表,头结点的前驱即可找到最后一个结点,可以快速插入,再向前可以找到最后一二个结点快速删除

    单链表找到链表尾部需要扫描整个链表

    双链表找到链表尾部也需要扫描整个链表

    单循环链表只有单向指针,找到链表尾部也需要扫描整个链表

     

    8、

    单链表的存储密度(  )

    • 大于1
    • 等于1
    • 小于1
    • 不能确定

    全麦白

    存储密度 = 数据项所占空间 / 结点所占空间

     

    9、完成在双循环链表结点p之后插入s的操作是

    • s->prior=p; s->next=p->next; p->next->prior=s ; p->next=s;

     

    10、用不带头结点的单链表存储队列,其队头指针指向队头结点,队尾指针指向队尾结点,则在进行出队操作时()

    • 仅修改队头指针
    • 仅修改队尾指针
    • 队头、队尾指针都可能要修改
    • 队头、队尾指针都要修改

     

    当只有一个元素,出队列时,要将队头和队尾,指向-1.所以说队头和队尾都需要修改

     

     

    链表刷了几百道吧,好题还有很多,以后接着更新

     

     

    第六次笔记(链表选讲/静态链表)

     

    本节课介绍了单链表的操作实现细节,介绍了静态链表。

     

    链表带头的作用:对链表进行操作时,可以对空表、非空表的情况以及 对首元结点进行统一处理,编程更方便。

    下面给出带头的单链表实现思路:

     

    按下标查找:

    判断非法输入,当 1 < =i <= n 时,i 的值是合法的。

    p = L -> next; j = 1;

    while ( p && j < i ) {  p = p ->next; ++j; }

    return 

     

    按值查找:

     p = L1 -> next;

     while ( p && p ->data!=key)          p = p -> next;

    return;

     

    插入:

    判断

    查找

    创建

    插入

     

    删除:

    查找

    删除

    释放内存

     

    静态链表

    对于线性链表,也可用一维数组来进行描述。这种描述方法便于在没有指针类型的高级程序设计语言中使用链表结构。

    这种存储结构,仍需要预先分配一个较大的空间,但在作为线性表的插入和删除操作时不需移动元素,仅需修改指针,故仍具有链式存储结构的主要优点。

     

    表示:

    #define MAXSIZE 1000      / /链表的最大长度

    typedef  struct{      

        ElemType data;        

        int cur;

    }component,  SLinkList[MAXSIZE];

     

    过程:

     

     

    顺序存储线性表实现

     

    在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。

     

    顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。

    优点:随机存取表中元素。缺点:插入和删除操作需要移动元素。

     

    线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)。

    给出两种基本实现:

    /*
    静态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INITSIZE 100
    #define ElemType int
    #define Status int
    #define OK     1
    #define ERROR  0
    
    typedef struct
    {
    	ElemType elem[LIST_INITSIZE];
    	int length;
    }SqList;
    
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DisCreat(SqList A,SqList *B,SqList *C);//拆分(按正负),也可以根据需求改
    //虽然思想略简单,但是要写的没有错误,还是需要锻炼coding能力的
    
    Status InitList(SqList *L)
    {
        L->length = 0;//长度为0
        return OK;
    }
    
    Status ListInsert(SqList *L, int i,ElemType e)
    {
        int j;
        if(i<1 || i>L->length+1)
            return ERROR;//判断非法输入
        if(L->length == LIST_INITSIZE)//判满
        {
            printf("表已满");//提示
            return ERROR;//返回失败
        }
        for(j = L->length;j > i-1;j--)//从后往前覆盖,注意i是从1开始
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        (L->length)++;//表长加1
        return OK;//反回成功
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i = 0;i < L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DisCreat(SqList A,SqList *B,SqList *C)
    {
        int i;
        for(i = 0;i < A.length;i++)//依次遍历A中元素
        {
            if(A.elem[i]<0)//判断
                ListInsert(B,B->length+1,A.elem[i]);//直接调用插入函数实现尾插
            else
                ListInsert(C,C->length+1,A.elem[i]);
        }
    }
    
    int main(void)
    {
        //复制的
    	SqList L;
    	SqList B, C;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,33,-3,-88,21,77,0,-9};
    	InitList(&L);
    	InitList(&B);
    	InitList(&C);
    	for (i = 1; i <= 9; i++)
    		ListInsert(&L,i,data[i-1]);
        printf("插入完成后L = : ");
    	ListPrint(L);
        ListDelete(&L,1,&e);
    	printf("删除第1个后L = : ");
    	ListPrint(L);
        DisCreat(L , &B, &C);
    	printf("拆分L后B = : ");
    	ListPrint(B);
    	printf("拆分L后C = : ");
    	ListPrint(C);
    	printf("拆分L后L = : ");
    	ListPrint(L);
    }

    静态:长度固定

    动态:不够存放可以加空间(搬家)

     

    /*
    子任务名任务:1_2 动态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INIT_SIZE 100
    #define LISTINCREMENT 10
    #define Status int
    #define OVERFLOW -1
    #define OK 1
    #define ERROR 0
    #define ElemType int
    
    typedef struct
    {
    	ElemType * elem;
    	int length;
    	int listsize;
    }SqList;
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DeleteMin(SqList *L);//删除最小
    
    Status InitList(SqList *L)
    {
        L->elem = (ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));//申请100空间
    	if(!L->elem)//申请失败
    		return ERROR;
    	L->length = 0;//长度0
    	L->listsize = LIST_INIT_SIZE;//容量100
    	return OK;//申请成功
    }
    
    Status ListInsert(SqList *L,int i,ElemType e)
    {
        int j;
        ElemType *newbase;
        if(i<1 || i>L->length+1)
            return ERROR;//非法输入
            
        if(L->length >= L->listsize)//存满了,需要更大空间
        {
            newbase = (ElemType*)realloc(L->elem,(L->listsize+LISTINCREMENT)*sizeof(ElemType));//大10的空间
            if(!newbase)//申请失败
                return ERROR;
            L->elem = newbase;//调指针
            L->listsize+= LISTINCREMENT;//新容量
        }
        
        for(j=L->length;j>i-1;j--)//从后往前覆盖
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        L->length++;//长度+1
        return OK;
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i=0;i<L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DeleteMin(SqList *L)
    {
        //表空在Listdelete函数里判断
        int i;
        int j=0;//最小值下标
        ElemType *e;
        for(i=0;i<L->length;i++)//寻找最小
        {
            if(L->elem[i] < L->elem[j])
                j=i;
        }
        ListDelete(L,j+1,&e);//调用删除,注意j要+1
    }
    
    int main(void)
    {
    	SqList L;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,-33,3,-88,21,77,0,-9};
    	InitList(&L);
    	for (i = 1; i <= 9; i++)
    	{
    		ListInsert(&L,i,data[i-1]);
    	}
    	printf("插入完成后 L = : ");
    	ListPrint(L);
        ListDelete(&L, 2, &e);
    	printf("删除第 2 个后L = : ");
    	ListPrint(L);
        DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    }

    单链表,不带头实现

    链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

    使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表双向链表以及循环链表

     

    下面给出不带头的单链表标准实现:

    定义节点:

    typedef struct node 
    { 
        int data;
        struct node * next;
    }Node;

    尾插:

    void pushBackList(Node ** list, int data) 
    { 
        Node * head = *list;
        Node * newNode = (Node *)malloc(sizeof(Node));//申请空间
        newNode->data = data; newNode->next = NULL;
        if(*list == NULL)//为空
            *list = newNode;
        else//非空
        {
            while(head ->next != NULL)
                head = head->next;
            head->next = newNode;
        }
    }
    
    

    插入:

    int insertList(Node ** list, int index, int data) 
    {
        int n;
        int size = sizeList(*list); 
        Node * head = *list; 
        Node * newNode, * temp;
        if(index<0 || index>size) return 0;//非法
        newNode = (Node *)malloc(sizeof(Node)); //创建新节点
        newNode->data = data; 
        newNode->next = NULL;
        if(index == 0) //头插
        {
            newNode->next = head; 
            *list = newNode; 
            return 1; 
        }
        for(n=1; n<index; n++) //非头插
            head = head->next;
        if(index != size) 
            newNode->next = head->next; 
        //链表尾部next不需指定
        head->next = newNode; 
        return 1;
    }
    

    按值删除:

    void deleteList(Node ** list, int data) 
    { 
        Node * head = *list; Node * temp; 
        while(head->next!=NULL) 
        { 
            if(head->next->data != data) 
            { 
                head=head->next; 
                continue; 
            } 
            temp = head->next;
            if(head->next->next == NULL) //尾节点删除
                head->next = NULL; 
            else 
                head->next = temp->next; 
            free(temp);
        }    
        head = *list; 
        if(head->data == data) //头结点删除
        { 
            temp = head; 
            *list = head->next; 
            head = head->next; 
            free(temp); 
        }
    }
    

    打印:

    void printList(Node * head) 
    { 
        Node * temp = head; 
        for(; temp != NULL; temp=temp->next) 
            printf("%d ", temp->data); 
        printf("\n"); 
    }

    清空:

    void freeList(Node ** list) 
    { 
        Node * head = *list; 
        Node * temp = NULL; 
        while(head != NULL) //依次释放
        { 
            temp = head; 
            head = head->next; 
            free(temp); 
        } 
        *list = NULL; //置空
    }

    别的也没啥了,都是基本操作

    有些代码要分情况,很麻烦,可读性较强吧

    看我压缩代码:https://blog.csdn.net/hebtu666/article/details/81261043

     

    双链表带头实现

    以前写的不带头的单链表实现,当时也啥也没学,好多东西不知道,加上一心想压缩代码,减少情况,所以写得不太好。

    请教了老师,首先是命名问题和代码紧凑性等的改进。还有可读性方面的改进,多写了一些注释。并且因为带头的比较好写,好操作,所以标准写法也不是很长,繁琐。

     

     

    下面贴代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    typedef struct node{
        int key;//数据
        struct node * prev;//前驱
        struct node * next;//后继
    }Node;

    初始化(带头) 

    Node * list;
    //初始化,这里·我们list不再是NULL,而是指向了一个节点
    //这个改进方便了很多操作,也不用借助二重指针把list和next统一表示了
    
    void init(Node * list)//初始化
    {
        list = (Node *)malloc(sizeof(Node));
        list->next = NULL;
        list->prev = NULL;
    }

    查找(不用再判断一下空不空)

    Node * find(int key,Node * list)
    {
        Node * head = list->next;//从头节点后面开始找
        while(head != NULL && head->key != key)//找到或空跳出
            head = head->next;
        return head;
    }

    打印

    void printList(Node * list)//打印
    {
        Node * temp = list->next;头节点下一个开始
        while(temp != NULL)
        {
            printf("%d ",temp->key);
            temp = temp->next;
        }
        printf("\n");
    }

    删除指定结点

    void delete(Node * list)//删除指定结点
    {
        list->prev->next = list->next;前面后面指针改了,再free自己即可
        list->next->prev = list->prev;
        free(list);
    }

    配合一下删除:

    void deleteKey(int key,Node * list)
    {
        delete(find(key,list));
    }

    头插:

    void insertHead(int key,Node * list)//头插
    {
        Node * newNode = (Node *)malloc(sizeof(Node));//初始化
        newNode->key = key;
        newNode->next = list->next;//赋值后继
        if(list->next != NULL)//如果有后继,赋值后继的前指针为新结点
            list->next->prev = newNode;
        list->next = newNode;//改头
        newNode->prev = list;//赋值新结点前指针
    }

    按下标插入

    单链表都写了,我就不写长度函数和判断非法了,用的时候注意吧。

    void insert(int key,Node * list,int index)
    {
        Node * head=list;//最后指到要插位置的前一个即可
        Node * newNode = (Node *)malloc(sizeof(Node));//初始化
        newNode->key = key;
        while(index--)
            head = head->next;
        newNode->next = head->next;//修改指针
        newNode->prev = head;
        head->next = newNode;
    }

    指定某值后插入不写了,和这个修改指针逻辑一样,再传一个find配合一下就行了。

     

    然后简单测试

    int main()
    {
        Node * list = NULL;
        init(list);
        insertHead(1,list);
        insertHead(2,list);
        insertHead(3,list);
        printList(list);
        deleteKey(2,list);
        printList(list);
        insert(10,list,0);
        printList(list);
    }

     

    第七次笔记(栈/队列)

     

    介绍栈和队列基本概念和用法。

     

    设输入序列1、2、3、4,则下述序列中( )不可能是出栈序列。【中科院中国科技大学2005】

    A. 1、2、3、4                      B. 4、 3、2、1

    C. 1、3、4、2                      D.4、1、2、3

    选D

    我是一个个模拟来做的。

     

    描述栈的基本型性质:

    1、集合性:栈是由若干个元素集合而成,没有元素(空集)成为空栈。

    2、线性:除栈顶和栈底之外,任意元素均有唯一前趋和后继。

    3、运算受限:只在一端插入删除的线性表即为栈

     

    顺序存储和顺序存取:顺序存取是只能逐个存或取结构中的元素,例如栈。顺序存储是利用一个连续的空间相继存放,例如栈可基于一维数组存放元素。

     

    一个较早入栈的元素能否在后面元素之前出栈?如果后面元素压在它上面,就不可以了。如果后面元素未压入,它可以弹出。在其他元素前面。

     

     

    栈与递归:

      当在一个函数的运行期间调用另一个函数时,在运行 该被调用函数之前,需先完成三件事:  将实参等传递给被调用函数,保存返回地址(入栈);  为被调用函数的局部变量分配存储区;    将控制转移到被调用函数的入口。  

    从被调用函数返回调用函数之前,应该完成:  保存被调函数的计算结果;  释放被调函数的数据区;  按被调函数保存的返回地址(出栈)将控制转移到调        用函数。

    多个函数嵌套调用的规则是:后调用先返回。

     此时的内存管理实行“栈式管理”

     

    队列:

            在多用户计算机系统中,各个用户需要使用 CPU 运行自己的程序,它们分别向操作系统提出使用 CPU 的请求,操作系统按照每个请求在时间上的先后顺序, 将其排成一个队列,每次把CPU分配给队头用户使用, 当相应的程序运行结束,则令其出队,再把CPU分配 给新的队头用户,直到所有用户任务处理完毕。

     

    以主机和打印机为例来说明,主机输出数据给打印 机打印,主机输出数据的速度比打印机打印的速度要快 得多,若直接把输出的数据送给打印机打印,由于速度 不匹配,显然不行。解决的方法是设置一个打印数据缓 冲区,主机把要打印的数据依此写到这个缓冲区中,写 满后就暂停输出,继而去做其它的事情,打印机就从缓 冲区中按照先进先出的原则依次取出数据并打印,打印 完后再向主机发出请求,主机接到请求后再向缓冲区写 入打印数据,这样利用队列既保证了打印数据的正确, 又使主机提高了效率。

     

    双端队列:

    某队列允许在其两端进行入队操作,但仅允许在一端进行出队操作,若元素a,b,c,d,e依次入队列后,再进行出队操作,则不可能得到的顺序是( )。 

    A. bacde                B. dbace              C. dbcae                D. ecbad

    解析:出队只能一端,所以abcde一定是这个顺序。

    反模拟入队,每次只能在两边出元素。

     

    栈/队列 互相模拟实现

     

    用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

    思路:大概这么想:用一个辅助栈把进第一个栈的元素倒一下就好了。

    比如进栈1,2,3,4,5

    第一个栈:

    5

    4

    3

    2

    1

    然后倒到第二个栈里

    1

    2

    3

    4

    5

    再倒出来,顺序为1,2,3,4,5

    实现队列

    然后要注意的事情:

    1)栈2非空不能往里面倒数,顺序就错了。栈2没数再从栈1倒。

    2)栈1要倒就一次倒完,不倒完的话,进新数也会循序不对。

    import java.util.Stack;
     
    public class Solution {
        Stack<Integer> stack1 = new Stack<Integer>();
        Stack<Integer> stack2 = new Stack<Integer>();
         
        public void push(int node) {
            stack1.push(node);
        }
         
        public int pop() {
            if(stack1.empty()&&stack2.empty()){
                throw new RuntimeException("Queue is empty!");
            }
            if(stack2.empty()){
                while(!stack1.empty()){
                    stack2.push(stack1.pop());
                }
            }
            return stack2.pop();
        }
    }

     

    用两个队列实现栈,要求同上:

    这其实意义不是很大,有些数据结构书上甚至说两个队列不能实现栈。

    其实是可以的,只是时间复杂度较高,一个弹出操作时间为O(N)。

    思路:两个队列,编号为1和2.

    进栈操作:进1号队列

    出栈操作:把1号队列全弄到2号队列里,剩最后一个别压入,而是返回。

    最后还得把1和2号换一下,因为现在是2号有数,1号空。

     

    仅仅有思考价值,不实用。

    比如压入1,2,3

    队列1:1,2,3

    队列2:空

    依次弹出1,2,3:

    队列1里的23进入2号,3弹出

    队列1:空

    队列2:2,3

     

    队列2中3压入1号,2弹出

    队列1:3

    队列2:空

     

    队列1中只有一个元素,弹出。

     

    上代码:

    public class TwoQueueImplStack {
    	Queue<Integer> queue1 = new ArrayDeque<Integer>();
    	Queue<Integer> queue2 = new ArrayDeque<Integer>();
    //压入
    	public void push(Integer element){
    		//都为空,优先1
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    		//1为空,2有数据,放入2
    		if(queue1.isEmpty()){
    			queue2.add(element);
    			return;
    		}
    		//2为空,1有数据,放入1
    		if(queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    	}
    //弹出
    	public Integer pop(){
    		//两个都空,异常
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			try{
    				throw new Exception("satck is empty!");
    			}catch(Exception e){
    				e.printStackTrace();
    			}
    		}	
    		//1空,2有数据,将2中的数据依次放入1,最后一个元素弹出
    		if(queue1.isEmpty()){
    			while(queue2.size() > 1){
    				queue1.add(queue2.poll());
    			}
    			return queue2.poll();
    		}
    		
    		//2空,1有数据,将1中的数据依次放入2,最后一个元素弹出
    		if(queue2.isEmpty()){
    			while(queue1.size() > 1){
    				queue2.add(queue1.poll());
    			}
    			return queue1.poll();
    		}
    		
    		return (Integer)null;
    	}
    //测试
    	public static void main(String[] args) {
    		TwoQueueImplStack qs = new TwoQueueImplStack();
    		qs.push(2);
    		qs.push(4);
    		qs.push(7);
    		qs.push(5);
    		System.out.println(qs.pop());
    		System.out.println(qs.pop());
    		
    		qs.push(1);
    		System.out.println(qs.pop());
    	}
    }
    

     

    第八次笔记 (串)

    串的概念:串(字符串):是由 0 个或多个字符组成的有限序列。 通常记为:s =‘ a1 a2 a3 … ai …an  ’ ( n≥0 )。

    串的逻辑结构和线性表极为相似。

     

    一些串的类型:

     

    空串:不含任何字符的串,长度 = 0。

    空格串:仅由一个或多个空格组成的串。

    子串:由串中任意个连续的字符组成的子序列。

    主串:包含子串的串。

    位置:字符在序列中的序号。

    子串在主串中的位置:子串的首字符在主串中的位置。

     

    空串是任意串的子串,任意串是其自身的子串。

    串相等的条件:当两个串的长度相等且各个对应位置的字符都相等时才相等。

     

    实现:

     

    因为串是特殊的线性表,故其存储结构与线性表的 存储结构类似,只不过组成串的结点是单个字符。

     

    定长顺序存储表示

    也称为静态存储分配的顺序串。 即用一组地址连续的存储单元依次存放串中的字符序列。

     

    串长:可能首字符记录(显式)或\0结尾(隐式)

     

    定长顺序存储表示时串操作的缺点 :串的某些操作受限(截尾),如串的联接、插入、置换

     

    堆分配存储表示  

     

    存储空间在程序执行过程中动态分配,malloc() 分配一块实际串长所需要的存储空间(“堆”)

    堆存储结构的优点:堆存储结构既有顺序存储 结构的特点,处理(随机取子串)方便,操作中对 串长又没有任何限制,更显灵活,因此在串处理的 应用程序中常被采用。

     

    串的块链存储表示

    为了提高空间利用率,可使每个结点存放多个字符 (这是顺序串和链串的综合 (折衷) ),称为块链结构。

     优点:便于插入和删除    缺点:空间利用率低 

     

    串的定长表示

    思想和代码都不难,和线性表也差不多,串本来就是数据受限的线性表。

    串连接:

     

    #include <stdio.h>
    #include <string.h>
    //串的定长顺序存储表示
    #define MAXSTRLEN 255							//用户可在255以内定义最大串长
    typedef unsigned char SString[MAXSTRLEN + 1];	//0号单元存放串的长度
    
    int Concat(SString *T,SString S1,SString S2)
    	//用T返回S1和S2联接而成的新串。若未截断返回1,若截断返回0
    {
    	int i = 1,j,uncut = 0;
    	if(S1[0] + S2[0] <= MAXSTRLEN)	//未截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    		for (j = 1; j <= S2[0]; j++)
    			(*T)[S1[0]+j] = S2[j];	//(*T)[i+j] = S2[j]
    		(*T)[0] = S1[0] + S2[0];
    		uncut = 1;
    	}
    	else if(S1[0] < MAXSTRLEN)		//截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    
    		for (j = S1[0] + 1; j <= MAXSTRLEN; j++)
    		{
    			(*T)[j] = S2[j - S1[0] ];
    			(*T)[0] = MAXSTRLEN;
    			uncut = 0;
    		}
    	}
    	else
    	{
    		for (i = 0; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		/*或者分开赋值,先赋值内容,再赋值长度
    		for (i = 1; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		(*T)[0] = MAXSTRLEN;
    		*/
    		uncut = 0;
    	}
    
    	return uncut;
    }
    
    int SubString(SString *Sub,SString S,int pos,int len)
    	//用Sub返回串S的第pos个字符起长度为len的子串
    	//其中,1 ≤ pos ≤ StrLength(S)且0 ≤ len ≤ StrLength(S) - pos + 1(从pos开始到最后有多少字符)
    	//第1个字符的下标为1,因为第0个字符存放字符长度
    {
    	int i;
    	if(pos < 1 || pos > S[0] || len < 0 || len > S[0] - pos + 1)
    		return 0;
    	for (i = 1; i <= len; i++)
    	{
    		//S中的[pos,len]的元素 -> *Sub中的[1,len]
    		(*Sub)[i] = S[pos + i - 1];//下标运算符 > 寻址运算符的优先级
    	}
    	(*Sub)[0] = len;
    	return 1;
    }
    void PrintStr(SString S)
    {
    	int i;
    	for (i = 1; i <= S[0]; i++)
    		printf("%c",S[i]);
    	printf("\n");
    }
    
    
    int main(void)
    {
    	/*定长顺序存储初始化和打印的方法
    	SString s = {4,'a','b','c','d'};
    	int i;
    	//s = "abc";	//不可直接赋值
    	for (i = 1; i <= s[0]; i++)
    		printf("%c",s[i]);
    	*/
    	SString s1 = {4,'a','b','c','d'};
    	SString s2 = {4,'e','f','g','h'},s3;
    	SString T,Sub;
    	int i;
    	
    	for (i = 1; i <= 255; i++)
    	{
    		s3[i] = 'a';
    		if(i >= 248)
    			s3[i] = 'K';
    	}
    	s3[0] = 255;
    	SubString(&Sub,s3,247,8);
    	PrintStr(Sub);
    	
    
    
    
    	return 0;
    }

    第九次笔记(数组,广义表)

    数组:按一定格式排列起来的具有相同类型的数据元素的集合。

     

    二维数组:若一维数组中的数据元素又是一维数组结构,则称为二维数组。 

    同理,推广到多维数组。若 n -1 维数组中的元素又是一个一维数组结构,则称作 n 维数组。 

    声明格式:数据类型   变量名称[行数] [列数] ;

     

    实现:一般都是采用顺序存储结构来表示数组。

     

    二维数组两种顺序存储方式:以行序为主序 (低下标优先) 、以列序为主序 (高下标优先)

    一个二维数组 A,行下标的范围是 1 到 6,列下标的范围是 0 到 7,每个数组元素用相邻的6 个字节存储,存储器按字节编址。那么,这个数组的体积是288个字节。

     

     广义表(又称列表 Lists)是n≥0个元素 a1, a2, …, an 的有限序列,其中每一个ai 或者是原子,或者是一个子表。

     

    表头:若 LS 非空 (n≥1 ),则其第一个元素 a1 就是表头。

     表尾:除表头之外的其它元素组成的表。记作  tail(LS) = (a2, ..., an)。 

     

    (( )) 长度为 1,表头、表尾均为 ( )

    (a, (b, c))长度为 2,由原子 a 和子表 (b, c) 构成。表头为 a ;表尾为 ((b, c))。

     

    广义表的长度定义为最外层所包含元素的个数

    广义表的深度定义为该广义表展开后所含括号的重数。

    “原子”的深度为 0 ;  “空表”的深度为 1 。

     

    取表头运算 GetHead  和取表尾运算 GetTail

    GetHead(LS) = a1        GetTail(LS) = (a2, …, an)。

     

    广义表可看成是线性表的推广,线性表是广义表的特例。

     

    广义表的结构相当灵活,在某种前提下,它可以兼容线 性表、数组、树和有向图等各种常用的数据结构。

    由于广义表不仅集中了线性表、数组、树和有向图等常 见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。 

     

     

    第十次笔记(树和二叉树)

     

    树的定义:树(Tree)是 n(n≥0)个结点的有限集。若 n=0,称为空树;若 n > 0,则它满足如下两个条件:  

    (1)  有且仅有一个特定的称为根 (Root) 的结点;  

    (2)  其余结点可分为 m (m≥0) 个互不相交的有限集 T1, T2, T3, …, Tm, 其中每一个集合本身又是一棵树,并称为根的子树 (SubTree)。

    显然,树的定义是一个递归的定义。

    树的一些术语:

    • 结点的度(Degree):结点的子树个数;
    • 树的度:树的所有结点中最大的度数;
    • 叶结点(Leaf):度为0的结点;
    • 父结点(Parent):有子树的结点是其子树的根节点的父结点;
    • 子结点/孩子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;
    • 兄弟结点(Sibling):具有同一个父结点的各结点彼此是兄弟结点;
    • 路径和路径长度:从结点n1到nk的路径为一个结点序列n1,n2,...,nk。ni是ni+1的父结点。路径所包含边的个数为路径的长度;
    • 祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点;
    • 子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙;
    • 结点的层次(Level):规定根结点在1层,其他任一结点的层数是其父结点的层数加1;
    • 树的深度(Depth):树中所有结点中的最大层次是这棵树的深度;

    将树中节点的各子树看成从左至右是有次序的(即不能互换),则称为该树是有序树,否则称为无序树

    森林(forest)是 m (m≥0) 棵互不相交的树的集合。

     

    二叉树

     

    在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。

     

    虽然二叉树与树概念不同,但有关树的基本术语对二叉树都适用。

     

    二叉树结点的子树要区分左子树和右子树,即使只有一 棵子树也要进行区分,说明它是左子树,还是右子树。树当 结点只有一个孩子时,就无须区分它是左还是右。

     

    注意:尽管二叉树与树有许多相似之处,但二叉树不是树的特殊情形。

    一些性质:

    在二叉树的第 i 层上至多有  个结点 (i ≥1)。

    证明:每个节点至多两个孩子,每一层至多比上一层多一倍的结点,根为1.

     

    深度为 k 的二叉树至多有 个结点(k ≥1)。

    证明:把每一层最大节点加起来即可

     

    对任何一棵二叉树 T,如果其叶子数为 n0,度为 2的结点数为 n2,则 n0 = n2 + 1。

    证明:对于一个只有根的树,n0 = n2 + 1成立。1=0+1

    我们把一个叶子节点换成度为2的结点:

    黑色节点原来为叶子节点

    我们发现,度为2的结点数加1(黑色节点);叶子节点数加1(原来的去掉,新增两个);对于式子n0 = n2 + 1没影响,还是成立。

     

    我们把叶子节点换成度为1的结点,比如没有右孩子。

    我们发现,度为2的结点数没变。叶子节点数没变(减了一个加了一个)

    所以,不管你怎么换,公式都成立。(佛系证明)

     

     

    二叉树概述

     

    各种实现和应用以后放链接

    一、二叉树的基本概念

    二叉树:二叉树是每个节点最多有两个子树的树结构。

    根节点:一棵树最上面的节点称为根节点。

    父节点子节点:如果一个节点下面连接多个节点,那么该节点称为父节点,它下面的节点称为子 节点。

    叶子节点:没有任何子节点的节点称为叶子节点。

    兄弟节点:具有相同父节点的节点互称为兄弟节点。

    节点度:节点拥有的子树数。上图中,13的度为2,46的度为1,28的度为0。

    树的深度:从根节点开始(其深度为0)自顶向下逐层累加的。上图中,13的深度是1,30的深度是2,28的深度是3。

    树的高度:从叶子节点开始(其高度为0)自底向上逐层累加的。54的高度是2,根节点23的高度是3。

    对于树中相同深度的每个节点来说,它们的高度不一定相同,这取决于每个节点下面的叶子节点的深度。上图中,13和54的深度都是1,但是13的高度是1,54的高度是2。

    二、二叉树的类型

    类型定义图示

    满二叉树

    Full Binary Tree

    除最后一层无任何子节点外,每一层上的所有节点都有两个子节点,最后一层都是叶子节点。满足下列性质:

    1)一颗树深度为h,最大层数为k,深度与最大层数相同,k=h;

    2)叶子节点数(最后一层)为2k−1;

    3)第 i 层的节点数是:2i−1;

    4)总节点数是:2k−1,且总节点数一定是奇数。

    完全二叉树

    Complete Binary Tree

    若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。满足下列性质:

    1)只允许最后一层有空缺结点且空缺在右边,即叶子节点只能在层次最大的两层上出现;

    2)对任一节点,如果其右子树的深度为j,则其左子树的深度必为j或j+1。 即度为1的点只有1个或0个;

    3)除最后一层,第 i 层的节点数是:2i−1;

    4)有n个节点的完全二叉树,其深度为:log2n+1或为log2n+1;

    5)满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

    平衡二叉树

    Balanced Binary Tree

    又被称为AVL树,它是一颗空树或左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。

    二叉搜索树

    Binary Search Tree

    又称二叉查找树、二叉排序树(Binary Sort Tree)。它是一颗空树或是满足下列性质的二叉树:

    1)若左子树不空,则左子树上所有节点的值均小于或等于它的根节点的值;

    2)若右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;

    3)左、右子树也分别为二叉排序树。

    红黑树

    Red Black Tree

    是每个节点都带有颜色属性(颜色为红色或黑色)的自平衡二叉查找树,满足下列性质:

    1)节点是红色或黑色;

    2)根节点是黑色;

    3)所有叶子节点都是黑色;

    4)每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)

    5)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

     

     

     

     

     

     

     

     

     

    啦啦啦

     

     

    第十一次笔记(满二叉树,完全二叉树)

    因为图片丢失,内容不全,我尽量找一下图

    满二叉树 (Full binary tree)

    除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树。

    国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。

    国外(国际)定义:a binary tree T is full if each node is either a leaf or possesses exactly two childnodes.

    大意为:如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。(一棵满二叉树的每一个结点要么是叶子结点,要么它有两个子结点,但是反过来不成立,因为完全二叉树也满足这个要求,但不是满二叉树)

    从图形形态上看,满二叉树外观上是一个三角形。

    这里缺失公式

    完全二叉树

     

    完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。

    可以根据公式进行推导,假设n0是度为0的结点总数(即叶子结点数),n1是度为1的结点总数,n2是度为2的结点总数,则 :

    ①n= n0+n1+n2 (其中n为完全二叉树的结点总数);又因为一个度为2的结点会有2个子结点,一个度为1的结点会有1个子结点,除根结点外其他结点都有父结点,

    ②n= 1+n1+2*n2 ;由①、②两式把n2消去得:n= 2*n0+n1-1,由于完全二叉树中度为1的结点数只有两种可能0或1,由此得到n0=n/2 或 n0=(n+1)/2。

    简便来算,就是 n0=n/2,其中n为奇数时(n1=0)向上取整;n为偶数时(n1=1)。可根据完全二叉树的结点总数计算出叶子结点数。

     

    重点:出于简便起见,完全二叉树通常采用数组而不是链表存储

     

    对于tree[i]有如下特点:

    (1)若i为奇数且i>1,那么tree的左兄弟为tree[i-1];

    (2)若i为偶数且i<n,那么tree的右兄弟为tree[i+1];

    (3)若i>1,tree的父亲节点为tree[i div 2];

    (4)若2*i<=n,那么tree的左孩子为tree[2*i];若2*i+1<=n,那么tree的右孩子为tree[2*i+1];

    (5)若i>n div 2,那么tree[i]为叶子结点(对应于(3));

    (6)若i<(n-1) div 2.那么tree[i]必有两个孩子(对应于(4))。

    (7)满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

    完全二叉树第i层至多有2^(i-1)个节点,共i层的完全二叉树最多有2^i-1个节点。

    特点:

    1)只允许最后一层有空缺结点且空缺在右边,即叶子结点只能在层次最大的两层上出现;

    2)对任一结点,如果其右子树的深度为j,则其左子树的深度必为j或j+1。 即度为1的点只有1个或0个

     

    第十二次笔记(二叉树的存储和遍历)

     

    顺序存储结构

     

    完全二叉树:用一组地址连续的 存储单元依次自上而下、自左至右存 储结点元素,即将编号为 i  的结点元 素存储在一维数组中下标为 i –1 的分量中。

    一般二叉树:将其每个结点与完 全二叉树上的结点相对照,存储在一 维数组的相应分量中。

     

    最坏情况:树退化为线性后:

    我们要把它“变”成这个大家伙来存了:

    深度为 k 的且只 有 k 个结点的右单支树需要 长度为2^k-1 的一维数组。 

     

    链式存储结构

    lchild和rchild都是指向相同结构的指针

    在 n 个结点的二叉链表中有 n + 1 个空指针域。

    typedef struct BiTNode { // 结点结构
        TElemType data;
        struct BiTNode *lchild,*rchild;// 左右孩子指针
    } BiTNode, *BiTree;
    

    可以多一条指向父的指针。

     

    遍历二叉树

     

    顺着某一条搜索路径巡访二叉树中的结点,使   得每个结点均被访问一次,而且仅被访问一次

     “访问”的含义很广,可以是对结点作各种处理, 如:输出结点的信息、修改结点的数据值等,但要求这种访问不破坏原来的数据结构。

    (所以有些题目比如morris遍历、链表后半段反转判断回文等等必须进行完,解题时就算已经得出答案也要遍历完,因为我们不能改变原来的数据结构。)

     

    具体遍历的介绍

    https://blog.csdn.net/hebtu666/article/details/82853988

     

    深入理解二叉树遍历

    二叉树:二叉树是每个节点最多有两个子树的树结构。

     

    本文介绍二叉树的遍历相关知识。

    我们学过的基本遍历方法,无非那么几个:前序,中序,后序,还有按层遍历等等。

    设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

    首先我们定义一颗二叉树

    typedef char ElementType;
    typedef struct TNode *Position;
    typedef Position BinTree;
    struct TNode{
        ElementType Data;
        BinTree Left;
        BinTree Right;
    };
    

    前序

    首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树

    思路:

    就是利用函数,先打印本个节点,然后对左右子树重复此过程即可。

    void PreorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        printf(" %c", BT->Data);
        PreorderTraversal(BT->Left);
        PreorderTraversal(BT->Right);
    }

     

    中序

    首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树

    思路:

    还是利用函数,先对左边重复此过程,然后打印根,然后对右子树重复。

    void InorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        InorderTraversal(BT->Left);
        printf(" %c", BT->Data);
        InorderTraversal(BT->Right);
    }

    后序

    首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根

    思路:

    先分别对左右子树重复此过程,然后打印根

    void PostorderTraversal(BinTree BT)
    {
        if(BT==NULL)return ;
        PostorderTraversal(BT->Left);
        PostorderTraversal(BT->Right);
        printf(" %c", BT->Data);
    }

    进一步思考

    看似好像很容易地写出了三种遍历。。。。。

     

    但是你真的理解为什么这么写吗?

    比如前序遍历,我们真的是按照定义里所讲的,首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。这种过程来遍历了一遍二叉树吗?

    仔细想想,其实有一丝不对劲的。。。

    再看代码:

    void Traversal(BinTree BT)//遍历
    {
    //1111111111111
        Traversal(BT->Left);
    //22222222222222
        Traversal(BT->Right);
    //33333333333333
    }

    为了叙述清楚,我给三个位置编了号 1,2,3

    我们凭什么能前序遍历,或者中序遍历,后序遍历?

    我们看,前序中序后序遍历,实现的代码其实是类似的,都是上面这种格式,只是我们分别在位置1,2,3打印出了当前节点而已啊。我们凭什么认为,在1打印,就是前序,在2打印,就是中序,在3打印,就是后序呢?不管在位置1,2,3哪里操作,做什么操作,我们利用函数遍历树的顺序变过吗?当然没有啊。。。

    都是三次返回到当前节点的过程:先到本个节点,也就是位置1,然后调用了其他函数,最后调用完了,我们开到了位置2。然后又调用别的函数,调用完了,我们来到了位置3.。然后,最后操作完了,这个函数才结束。代码里的三个位置,每个节点都被访问了三次。

    而且不管位置1,2,3打印了没有,操作了没有,这个顺序是永远存在的,不会因为你在位置1打印了,顺序就改为前序,你在位置2打印了,顺序就成了中序。

     

    为了有更直观的印象,我们做个试验:在位置1,2,3全都放入打印操作;

    我们会发现,每个节点都被打印了三次。而把每个数第一次出现拿出来,就组成了前序遍历的序列;所有数字第二次出现拿出来,就组成了中序遍历的序列。。。。

     

    其实,遍历是利用了一种数据结构:栈

    而我们这种写法,只是通过函数,来让系统帮我们压了栈而已。为什么能实现遍历?为什么我们访问完了左子树,能返回到当前节点?这都是栈的功劳啊。我们把当前节点(对于函数就是当时的现场信息)存到了栈里,记录下来,后来才能把它拿了出来,能回到以前的节点。

     

    想到这里,可能就有更深刻的理解了。

    我们能否不用函数,不用系统帮我们压栈,而是自己做一个栈,来实现遍历呢?

    先序实现思路:拿到一个节点的指针,先判断是否为空,不为空就先访问(打印)该结点,然后直接进栈,接着遍历左子树;为空则要从栈中弹出一个节点来,这个时候弹出的结点就是其父亲,然后访问其父亲的右子树,直到当前节点为空且栈为空时,结束。

    核心思路代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            //visit(p);进行操作
            st.push(p);//入栈
            p = p->lchild;左
        } 
        else//空
        {
            p = st.top();//取出
            st.pop();
            p = p->rchild;//右
        }
    }

    中序实现思路:和前序遍历一样,只不过在访问节点的时候顺序不一样,访问节点的时机是从栈中弹出元素时访问,如果从栈中弹出元素,就意味着当前节点父亲的左子树已经遍历完成,这时候访问父亲,就是中序遍历.

    (对应递归是第二次遇到)

    核心代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            st.push(p);//压入
            p = p->lchild;
        }
        else//空
        {
            p = st.top();//取出
            //visit(p);操作
            st.pop();
            p = p->rchild;
        }
    }

    后序遍历是最难的。因为要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难点。

    因为我们原来说了,后序是第三次遇到才进行操作的,所以我们很容易有这种和递归函数类似的思路:对于任一结点,将其入栈,然后沿其左子树一直往下走,一直走到没有左孩子的结点,此时该结点在栈顶,但是不能出栈访问, 因此右孩子还没访问。所以接下来按照相同的规则对其右子树进行相同的处理。访问完右孩子,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

    第二种思路:对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,或者左孩子和右孩子都已被访问过了,就可以直接访问该结点。如果有孩子未访问,将P的右孩子和左孩子依次入栈。

    网上的思路大多是第一种,所以我在这里给出第二种的大概实现吧

    首先初始化cur,pre两个指针,代表访问的当前节点和之前访问的节点。把根放入,开始执行。

    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        if((cur->lchild==NULL && cur->rchild==NULL)||(pre!=NULL && (pre==cur->lchild||pre==cur->rchild)))
        {
            //visit(cur);  如果当前结点没有孩子结点或者孩子节点都已被访问过 
            s.pop();//弹出
            pre=cur; //记录
        }
        else//分别放入右左孩子
        {
            if(cur->rchild!=NULL)
                s.push(cur->rchild);
            if(cur->lchild!=NULL)    
                s.push(cur->lchild);
        }
    }

    这两种方法,都是利用栈结构来实现的遍历,需要一定的栈空间,而其实存在一种时间O(N),空间O(1)的遍历方式,下次写了我再放链接。

     

    斗个小机灵:后序是LRD,我们其实已经知道先序是DLR,那其实我们可以用先序来实现后序啊,我们只要先序的时候把左右子树换一下:DRL(这一步很好做到),然后倒过来不就是DRL了嘛。。。。。就把先序代码改的左右反过来,然后放栈里倒过来就好了,不需要上面介绍的那些复杂的方法。。。。

    第十四次笔记(树的存储)

     

     

    父节点表示法

     

    数据域:存放结点本身信息。

    双亲域:指示本结点的双亲结点在数组中的位置。

    对应的树:

    /* 树节点的定义 */
    #define MAX_TREE_SIZE 100
     
    typedef struct{
        TElemType data;
        int parent; /* 父节点位置域 */
    } PTNode;
     
    typedef struct{
        PTNode nodes[MAX_TREE_SIZE];
        int n; /* 节点数 */
    } PTree;

    特点:找双亲容易,找孩子难。

    孩子表示法(树的链式存储结构)

     

    childi指向一个结点

    可以加上parent。

    在有 n 个结点、度为  d 的树的 d 叉链表中,有  n×(d-1)+1 个空链域

     

    我们可以用degree记录有几个孩子,省掉空间,但是结点的指针个数不相等,为该结点的度 degree。

     

    孩子链表:

     

    把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则 n 个结点有 n 个孩子链表(叶子的孩子链表为空表)。而 n 个头指针又组成一个线性表,用顺序表(含 n 个元素的结构数组)存储。

    孩子兄弟表示法(二叉树表示法)

    用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点

    typedef struct CSNode{
         ElemType data;
         struct CSNode *firstchild, *nextsibling;  
    } CSNode, *CSTree;
    

    第十五次笔记(图基础)

     

    图是一种:   数据元素间存在多对多关系的数据结构   加上一组基本操作构成的抽象数据类型。

    图 (Graph) 是一种复杂的非线性数据结构,由顶点集合及顶点间的关系(也称弧或边)集合组成。可以表示为: G=(V, VR)  

    其中 V 是顶点的有穷非空集合;

    VR 是顶点之间   关系的有穷集合,也叫做弧或边集合。

    弧是顶点的有序对,边是顶点的无序对。

     

    特点:(相对于线性结构)

    顶点之间的关系是任意的 

    图中任意两个顶点之间都可能相关

    顶点的前驱和后继个数无限制

     

    相关概念:

     

    顶点(Vertex):图中的数据元素。线性表中我们把数据元素叫元素,树中将数据元素叫结点。

    边:顶点之间的逻辑关系用边来表示,边集可以是空的。

     

    无向边(Edge):若顶点V1到V2之间的边没有方向,则称这条边为无向边。

    无向图(Undirected graphs):图中任意两个顶点之间的边都是无向边。(A,D)=(D,A)

    无向图中边的取值范围:0≤e≤n(n-1)/2

    有向边:若从顶点V1到V2的边有方向,则称这条边为有向边,也称弧(Arc)。用<V1,V2>表示,V1为狐尾(Tail),V2为弧头(Head)。(V1,V2)≠(V2,V1)。

    有向图(Directed graphs):图中任意两个顶点之间的边都是有向边。

    有向图中弧的取值范围:0≤e≤n(n-1)

       注意:无向边用“()”,而有向边用“< >”表示。

     

    简单图:图中不存在顶点到其自身的边,且同一条边不重复出现。

    无向完全图:无向图中,任意两个顶点之间都存在边。

    有向完全图:有向图中,任意两个顶点之间都存在方向互为相反的两条弧。

    稀疏图:有很少条边。

    稠密图:有很多条边。

     

    邻接点:若 (v, v´) 是一条边,则称顶点 v 和 v´互为 邻接点,或称 v 和 v´相邻接;称边 (v, v´) 依附于顶点 v 和 v´,或称 (v, v´) 与顶点 v 和 v´ 相关联。

     

    权(Weight):与图的边或弧相关的数。

    网(Network):带权的图。

    子图(Subgraph):假设G=(V,{E})和G‘=(V',{E'}),如果V'包含于V且E'包含于E,则称G'为G的子图。

     

     入度:有向图中以顶点 v 为头的弧的数目称为 v 的入度,记为:ID(v)。  

    出度:有向图中以顶点 v 为尾的弧的数目称为 v 的出度,记为:OD(v)。

    度(Degree):无向图中,与顶点V相关联的边的数目。有向图中,入度表示指向自己的边的数目,出度表示指向其他边的数目,该顶点的度等于入度与出度的和。

     

    回路(环):第一个顶点和最后一个顶点相同的路径。

    简单路径:序列中顶点(两端点除外)不重复出现的路径。 

    简单回路(简单环):前后两端点相同的简单路径。

    路径的长度:一条路径上边或弧的数量。

     

    连通:从顶点 v 到 v´ 有路径,则说 v  和 v´ 是连通的。

    连通图:图中任意两个顶点都是连通的。

    连通分量:无向图的极大连通子图(不存在包含它的 更大的连通子图);

    任何连通图的连通分量只有一个,即其本身;非连通图有多个连通分量(非连通图的每一个连通部分)。

    强连通图: 任意两个顶点都连通的有向图。 

    强连通分量:有向图的极大强连通子图;任何强连通 图的强连通分量只有一个,即其本身;非强连通图有多个 强连通分量。

     

    生成树:所有顶点均由边连接在一起但不存在回路的图。(n个顶点n-1条边)

     

     

     

     

    图的存储

     

    多重链表:完全模拟图的样子,每个节点内的指针都指向该指向的节点。

    节点结构内指针数为度

    缺点:浪费空间、不容易操作

     

    数组表示法(邻接矩阵表示法)

    可用两个数组存储。其中一个 一维数组存储数据元素(顶点)的信息,另一个二维数组 (邻接矩阵)存储数据元素之间的关系(边或弧)的信息

    有向图:

    有向网:

    缺点:用于稀疏图时空间浪费严重

    优点:操作较容易

     

    邻接表

    指针数组存放每个结点,每个结点后接所有能到达的节点。

     

    图的遍历

     

    从图的任意指定顶点出发,依照某种规则去访问图中所有顶 点,且每个顶点仅被访问一次,这一过程叫做图的遍历。

    图的遍历按照深度优先和广度优先规则去实施,通常有深度 优先遍历法(Depth_First Search——DFS )和  广度优先遍历法 ( Breadth_Frist Search——BFS)两种。

    简单棋盘搜索https://blog.csdn.net/hebtu666/article/details/81483407

    别的实现以后再贴

    如何判别V的邻接点是否被访问?

    为每个顶点设立一个“访问标志”。

     

    最小生成树

    问题提出:
        要在n个城市间建立通信联络网。顶点:表示城市,权:城市间通信线路的花费代价。希望此通信网花费代价最小。
    问题分析:
        答案只能从生成树中找,因为要做到任何两个城市之间有线路可达,通信网必须是连通的;但对长度最小的要求可以知道网中显然不能有圈,如果有圈,去掉一条边后,并不破坏连通性,但总代价显然减少了,这与总代价最小的假设是矛盾的。
    结论:
        希望找到一棵生成树,它的每条边上的权值之和(即建立该通信网所需花费的总代价)最小 —— 最小代价生成树。
        构造最小生成树的算法很多,其中多数算法都利用了一种称之为 MST 的性质。
        MST 性质:设 N = (V, E)  是一个连通网,U是顶点集 V的一个非空子集。若边 (u, v) 是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边 (u, v) 的最小生成树。


    (1)普里姆 (Prim) 算法

    算法思想: 
        ①设 N=(V, E)是连通网,TE是N上最小生成树中边的集合。
        ②初始令 U={u_0}, (u_0∈V), TE={ }。
        ③在所有u∈U,u∈U-V的边(u,v)∈E中,找一条代价最小的边(u_0,v_0 )。
        ④将(u_0,v_0 )并入集合TE,同时v_0并入U。
        ⑤重复上述操作直至U = V为止,则 T=(V,TE)为N的最小生成树。

     
    代码实现:

    void MiniSpanTree_PRIM(MGraph G,VertexType u)
        //用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。
        //记录从顶点集U到V-U的代价最小的边的辅助数组定义;
        //closedge[j].lowcost表示在集合U中顶点与第j个顶点对应最小权值
    {
        int k, j, i;
        k = LocateVex(G,u);
        for (j = 0; j < G.vexnum; ++j)    //辅助数组的初始化
            if(j != k)
            {
                closedge[j].adjvex = u;
                closedge[j].lowcost = G.arcs[k][j].adj;    
    //获取邻接矩阵第k行所有元素赋给closedge[j!= k].lowcost
            }
        closedge[k].lowcost = 0;        
    //初始,U = {u};  
        PrintClosedge(closedge,G.vexnum);
        for (i = 1; i < G.vexnum; ++i)    \
    //选择其余G.vexnum-1个顶点,因此i从1开始循环
        {
            k = minimum(G.vexnum,closedge);        
    //求出最小生成树的下一个结点:第k顶点
            PrintMiniTree_PRIM(G, closedge, k);     //输出生成树的边
            closedge[k].lowcost = 0;                //第k顶点并入U集
            PrintClosedge(closedge,G.vexnum);
            for(j = 0;j < G.vexnum; ++j)
            {                                           
                if(G.arcs[k][j].adj < closedge[j].lowcost)    
    //比较第k个顶点和第j个顶点权值是否小于closedge[j].lowcost
                {
                    closedge[j].adjvex = G.vexs[k];//替换closedge[j]
                    closedge[j].lowcost = G.arcs[k][j].adj;
                    PrintClosedge(closedge,G.vexnum);
                }
            }
        }
    }


    (2)克鲁斯卡尔 (Kruskal) 算法

    算法思想: 
        ①设连通网  N = (V, E ),令最小生成树初始状态为只有n个顶点而无边的非连通图,T=(V, { }),每个顶点自成一个连通分量。
        ②在 E 中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
    ③依此类推,直至 T 中所有顶点都在同一连通分量上为止。
          
        最小生成树可能不惟一!

     

    拓扑排序

    (1)有向无环图

        无环的有向图,简称 DAG (Directed Acycline Graph) 图。
     
    有向无环图在工程计划和管理方面的应用:除最简单的情况之外,几乎所有的工程都可分为若干个称作“活动”的子工程,并且这些子工程之间通常受着一定条件的约束,例如:其中某些子工程必须在另一些子工程完成之后才能开始。
    对整个工程和系统,人们关心的是两方面的问题: 
    ①工程能否顺利进行; 
    ②完成整个工程所必须的最短时间。

    对应到有向图即为进行拓扑排序和求关键路径。 
    AOV网: 
        用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。
    例如:排课表
          
    AOV网的特点:
    ①若从i到j有一条有向路径,则i是j的前驱;j是i的后继。
    ②若< i , j >是网中有向边,则i是j的直接前驱;j是i的直接后继。
    ③AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。


    问题:    
        问题:如何判别 AOV 网中是否存在回路?
        检测 AOV 网中是否存在环方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。


    拓扑排序的方法:
        ①在有向图中选一个没有前驱的顶点且输出之。
        ②从图中删除该顶点和所有以它为尾的弧。
        ③重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止。
            
        一个AOV网的拓扑序列不是唯一的!
    代码实现:

    Status TopologicalSort(ALGraph G)
        //有向图G采用邻接表存储结构。
        //若G无回路,则输出G的顶点的一个拓扑序列并返回OK,否则返回ERROR.
        //输出次序按照栈的后进先出原则,删除顶点,输出遍历
    {
        SqStack S;
        int i, count;
        int *indegree1 = (int *)malloc(sizeof(int) * G.vexnum);
        int indegree[12] = {0};
        FindInDegree(G, indegree);    //求个顶点的入度下标从0开始
        InitStack(&S);
        PrintStack(S);
        for(i = 0; i < G.vexnum; ++i)
            if(!indegree[i])        //建0入度顶点栈S
                push(&S,i);        //入度为0者进栈
        count = 0;                //对输出顶点计数
        while (S.base != S.top)
        {
            ArcNode* p;
            pop(&S,&i);
            VisitFunc(G,i);//第i个输出栈顶元素对应的顶点,也就是最后进来的顶点    
            ++count;          //输出i号顶点并计数
            for(p = G.vertices[i].firstarc; p; p = p->nextarc)
            {    //通过循环遍历第i个顶点的表结点,将表结点中入度都减1
                int k = p->adjvex;    //对i号顶点的每个邻接点的入度减1
                if(!(--indegree[k]))
                    push(&S,k);        //若入度减为0,则入栈
            }//for
        }//while
        if(count < G.vexnum)
        {
            printf("\n该有向图有回路!\n");
            return ERROR;    //该有向图有回路
        }
        else
        {
            printf("\n该有向图没有回路!\n");
            return OK;
        }
    }


    关键路径

        把工程计划表示为有向图,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。称这种有向图为边表示活动的网,简称为 AOE网 (Activity On Edge)。
    例如:
    设一个工程有11项活动,9个事件。
    事件v_1——表示整个工程开始(源点) 
    事件v_9——表示整个工程结束(汇点)

     
    对AOE网,我们关心两个问题:  
    ①完成整项工程至少需要多少时间? 
    ②哪些活动是影响工程进度的关键?
    关键路径——路径长度最长的路径。
    路径长度——路径上各活动持续时间之和。
    v_i——表示事件v_i的最早发生时间。假设开始点是v_1,从v_1到〖v�i〗的最长路径长度。ⅇ(ⅈ)——表示活动a_i的最早发生时间。
    l(ⅈ)——表示活动a_i最迟发生时间。在不推迟整个工程完成的前提下,活动a_i最迟必须开始进行的时间。
    l(ⅈ)-ⅇ(ⅈ)意味着完成活动a_i的时间余量。
    我们把l(ⅈ)=ⅇ(ⅈ)的活动叫做关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程进度。
        例如上图中网,从从v_1到v_9的最长路径是(v_1,v_2,v_5,v_8,ν_9 ),路径长度是18,即ν_9的最迟发生时间是18。而活动a_6的最早开始时间是5,最迟开始时间是8,这意味着:如果a_6推迟3天或者延迟3天完成,都不会影响整个工程的完成。因此,分析关键路径的目的是辨别哪些是关键活动,以便争取提高关键活动的工效,缩短整个工期。
        由上面介绍可知:辨别关键活动是要找l(ⅈ)=ⅇ(ⅈ)的活动。为了求ⅇ(ⅈ)和l(ⅈ),首先应求得事件的最早发生时间vⅇ(j)和最迟发生时间vl(j)。如果活动a_i由弧〈j,k〉表示,其持续时间记为dut(〈j,k〉),则有如下关系:
    ⅇ(ⅈ)= vⅇ(j)
    l(ⅈ)=vl(k)-dut(〈j,k〉)
        求vⅇ(j)和vl(j)需分两步进行:
    第一步:从vⅇ(0)=0开始向前递推
    vⅇ(j)=Max{vⅇ(i)+dut(〈j,k〉)}   〈i,j〉∈T,j=1,2,…,n-1
    其中,T是所有以第j个顶点为头的弧的集合。
    第二步:从vl(n-1)=vⅇ(n-1)起向后递推
    vl(i)=Min{vl(j)-dut(〈i,j〉)}  〈i,j〉∈S,i=n-2,…,0
    其中,S是所有以第i个顶点为尾的弧的集合。
    下面我们以上图AOE网为例,先求每个事件v_i的最早发生时间,再逆向求每个事件对应的最晚发生时间。再求每个活动的最早发生时间和最晚发生时间,如下面表格:
              
    在活动的统计表中,活动的最早发生时间和最晚发生时间相等的,就是关键活动


    关键路径的讨论:

    ①若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。      如:a11、a10、a8、a7。 
    ②如果一个活动处于所有的关键路径上,则提高这个活动的速度,就能缩短整个工程的完成时间。如:a1、a4。
    ③处于所有关键路径上的活动完成时间不能缩短太多,否则会使原关键路径变成非关键路径。这时必须重新寻找关键路径。如:a1由6天变成3天,就会改变关键路径。

    关键路径算法实现:

    int CriticalPath(ALGraph G)
    {    //因为G是有向网,输出G的各项关键活动
        SqStack T;
        int i, j;    ArcNode* p;
        int k , dut;
        if(!TopologicalOrder(G,T))
            return 0;
        int vl[VexNum];
        for (i = 0; i < VexNum; i++)
            vl[i] = ve[VexNum - 1];        //初始化顶点事件的最迟发生时间
        while (T.base != T.top)            //按拓扑逆序求各顶点的vl值
        {
     
            for(pop(&T, &j), p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                k = p->adjvex;    dut = *(p->info);    //dut<j, k>
                if(vl[k] - dut < vl[j])
                    vl[j] = vl[k] - dut;
            }//for
        }//while
        for(j = 0; j < G.vexnum; ++j)    //求ee,el和关键活动
        {
            for (p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                int ee, el;        char tag;
                k = p->adjvex;    dut = *(p->info);
                ee = ve[j];    el = vl[k] - dut;
                tag = (ee == el) ? '*' : ' ';
                PrintCriticalActivity(G,j,k,dut,ee,el,tag);
            }
        }
        return 1;
    }

     

    最短路 

    最短路

        典型用途:交通网络的问题——从甲地到乙地之间是否有公路连通?在有多条通路的情况下,哪一条路最短?
     
        交通网络用有向网来表示:顶点——表示城市,弧——表示两个城市有路连通,弧上的权值——表示两城市之间的距离、交通费或途中所花费的时间等。
        如何能够使一个城市到另一个城市的运输时间最短或运费最省?这就是一个求两座城市间的最短路径问题。
        问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n - 1条边。
       常见最短路径问题:单源点最短路径、所有顶点间的最短路径
    (1)如何求得单源点最短路径?
        穷举法:将源点到终点的所有路径都列出来,然后在其中选最短的一条。但是,当路径特别多时,特别麻烦;没有规律可循。
        迪杰斯特拉(Dijkstra)算法:按路径长度递增次序产生各顶点的最短路径。
    路径长度最短的最短路径的特点:
        在此路径上,必定只含一条弧 <v_0, v_1>,且其权值最小。由此,只要在所有从源点出发的弧中查找权值最小者。
    下一条路径长度次短的最短路径的特点:
    ①、直接从源点到v_2<v_0, v_2>(只含一条弧);
    ②、从源点经过顶点v_1,再到达v_2<v_0, v_1>,<v_1, v_2>(由两条弧组成)
    再下一条路径长度次短的最短路径的特点:
        有以下四种情况:
        ①、直接从源点到v_3<v_0, v_3>(由一条弧组成);
        ②、从源点经过顶点v_1,再到达v_3<v_0, v_1>,<v_1, v_3>(由两条弧组成);
        ③、从源点经过顶点v_2,再到达v_3<v_0, v_2>,<v_2, v_3>(由两条弧组成);
        ④、从源点经过顶点v_1  ,v_2,再到达v_3<v_0, v_1>,<v_1, v_2>,<v_2, v_3>(由三条弧组成);
    其余最短路径的特点:    
        ①、直接从源点到v_i<v_0, v_i>(只含一条弧);
        ②、从源点经过已求得的最短路径上的顶点,再到达v_i(含有多条弧)。
    Dijkstra算法步骤:
        初始时令S={v_0},  T={其余顶点}。T中顶点对应的距离值用辅助数组D存放。
        D[i]初值:若<v_0, v_i>存在,则为其权值;否则为∞。 
        从T中选取一个其距离值最小的顶点v_j,加入S。对T中顶点的距离值进行修改:若加进v_j作中间顶点,从v_0到v_i的距离值比不加 vj 的路径要短,则修改此距离值。
        重复上述步骤,直到 S = V 为止。

    算法实现:

    void ShortestPath_DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D)
    { // 用Dijkstra算法求有向网 G 的 v0 顶点到其余顶点v的最短路径P[v]及带权长度D[v]。
        // 若P[v][w]为TRUE,则 w 是从 v0 到 v 当前求得最短路径上的顶点。  P是存放最短路径的矩阵,经过顶点变成TRUE
        // final[v]为TRUE当且仅当 v∈S,即已经求得从v0到v的最短路径。
        int v,w,i,j,min;
        Status final[MAX_VERTEX_NUM];
        for(v = 0 ;v < G.vexnum ;++v)
        {
            final[v] = FALSE;
            D[v] = G.arcs[v0][v].adj;        //将顶点数组中下标对应是 v0 和 v的距离给了D[v]
            for(w = 0;w < G.vexnum; ++w)
                P[v][w] = FALSE;            //设空路径
            if(D[v] < INFINITY)
            {
                P[v][v0] = TRUE;
                P[v][v] = TRUE;
            }
        }
        D[v0]=0;
        final[v0]= TRUE; /* 初始化,v0顶点属于S集 */
        for(i = 1;i < G.vexnum; ++i) /* 其余G.vexnum-1个顶点 */
        { /* 开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集 */
            min = INFINITY; /* 当前所知离v0顶点的最近距离 */
            for(w = 0;w < G.vexnum; ++w)
                if(!final[w]) /* w顶点在V-S中 */
                    if(D[w] < min)
                    {
                        v = w;
                        min = D[w];
                    } /* w顶点离v0顶点更近 */
                    final[v] = TRUE; /* 离v0顶点最近的v加入S集 */
                    for(w = 0;w < G.vexnum; ++w) /* 更新当前最短路径及距离 */
                    {
                        if(!final[w] && min < INFINITY && G.arcs[v][w].adj < INFINITY && (min + G.arcs[v][w].adj < D[w]))
                        { /* 修改D[w]和P[w],w∈V-S */
                            D[w] = min + G.arcs[v][w].adj;
                            for(j = 0;j < G.vexnum;++j)
                                P[w][j] = P[v][j];
                            P[w][w] = TRUE;
                        }
                    }
        }
    }

     

    经典二分问题

    经典二分问题:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  。

    写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
    示例 1:

    输入: nums = [-1,0,3,5,9,12], target = 9。输出: 4
    解释: 9 出现在 nums 中并且下标为 4
    示例 2:

    输入: nums = [-1,0,3,5,9,12], target = 2。输出: -1
    解释: 2 不存在 nums 中因此返回 -1

    思路1:我们当然可以一个数一个数的遍历,但是毫无疑问要被大妈鄙视,这可怎么办呢?

    思路2:二分查找
    二分查找是一种基于比较目标值和数组中间元素的教科书式算法。

    如果目标值等于中间元素,则找到目标值。
    如果目标值较小,证明目标值小于中间元素及右边的元素,继续在左侧搜索。
    如果目标值较大,证明目标值大于中间元素及左边的元素,继续在右侧搜索。

    算法代码描述:

    初始化指针 left = 0, right = n - 1。
    当 left <= right:
    比较中间元素 nums[pivot] 和目标值 target 。
    如果 target = nums[pivot],返回 pivot。
    如果 target < nums[pivot],则在左侧继续搜索 right = pivot - 1。
    如果 target > nums[pivot],则在右侧继续搜索 left = pivot + 1。

    算法实现:照例贴出三种语言的实现,在Java实现中给出了详细注释

    class Solution {
      public int search(int[] nums, int target) {
    	//分别准备好左右端点
        int left = 0, right = nums.length - 1;
    	//循环二分
        while (left <= right) {
    	//取中点
          int pivot = left + (right - left) / 2;
    	  //找到答案并返回
          if (nums[pivot] == target) return pivot;
    	  //向左继续找
          if (target < nums[pivot]) right = pivot - 1;
    	  //向右继续找
          else left = pivot + 1;
        }
    	//未找到,返回-1
        return -1;
      }
    }
    class Solution:
        def search(self, nums: List[int], target: int) -> int:
            left, right = 0, len(nums) - 1
            while left <= right:
                pivot = left + (right - left) // 2
                if nums[pivot] == target:
                    return pivot
                if target < nums[pivot]:
                    right = pivot - 1
                else:
                    left = pivot + 1
            return -1
    class Solution {
      public:
      int search(vector<int>& nums, int target) {
        int pivot, left = 0, right = nums.size() - 1;
        while (left <= right) {
          pivot = left + (right - left) / 2;
          if (nums[pivot] == target) return pivot;
          if (target < nums[pivot]) right = pivot - 1;
          else left = pivot + 1;
        }
        return -1;
      }
    };

     

     

     

    二叉搜索树实现

    本文给出二叉搜索树介绍和实现

     

    首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

     

    那这个结构有什么有用呢?

    首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

    基本操作:

    1、插入某个数值

    2、查询是否包含某个数值

    3、删除某个数值

     

    根据实现不同,还可以实现其他很多种操作。

     

    实现思路思路:

    前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

    而删除稍微复杂一些,有下面这几种情况:

    1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

    2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

    3、以上都不满足,就把左儿子子孙中最大节点提上来。

     

    当然,反过来也是成立的,比如右儿子子孙中最小的节点。

     

    下面来叙述为什么可以这么做。

    下图中A为待删除节点。

    第一种情况:

     

    1、去掉A,把c提上来,c也是小于x的没问题。

    2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

     

    第二种情况

     

    3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

    4、同理

     

    第三种情况

     

    5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

    因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

    而黑点小于A,也就小于c,所以可以让c当右孩子

    大概证明就这样。。

    下面我们用代码实现并通过注释理解

    上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

    定义

    struct node
    {
        int val;//数据
        node *lch,*rch;//左右孩子
    };

    插入

     node *insert(node *p,int x)
     {
         if(p==NULL)//直到空就创建节点
         {
             node *q=new node;
             q->val=x;
             q->lch=q->rch=NULL;
             return p;
         }
         if(x<p->val)p->lch=insert(p->lch,x);
         else p->lch=insert(p->rch,x);
         return p;//依次返回自己,让上一个函数执行。
     }

    查找

     bool find(node *p,int x)
     {
         if(p==NULL)return false;
         else if(x==p->val)return true;
         else if(x<p->val)return find(p->lch,x);
         else return find(p->rch,x);
     }

    删除

     node *remove(node *p,int x)
     {
          if(p==NULL)return NULL;
          else if(x<p->val)p->lch=remove(p->lch,x);
          else if(x>p->val)p->lch=remove(p->rch,x);
          //以下为找到了之后
          else if(p->lch==NULL)//情况1
          {
              node *q=p->rch;
              delete p;
              return q;
          }
          else if(p->lch->rch)//情况2
          {
              node *q=p->lch;
              q->rch=p->rch;
              delete p;
              return q;
          }
          else
          {
              node *q;
              for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个
              node *r=q->rch;//最大节点
              q->rch=r->lch;//最大节点左孩子提到最大节点位置
              r->lch=p->lch;//调整黑点左孩子为B
              r->rch=p->rch;//调整黑点右孩子为c
              delete p;//删除
              return r;//返回给父
          }
          return p;
     }

     

    对数组排序可以说是编程基础中的基础,本文对八种排序方法做简要介绍并用python实现。

    代码中注释很全,适合复习和萌新学习。这是刚入学自己写的,可能难免比不上标准的写法,但是懒得改了。

    文末会放和排序相关的基本拓展总结链接。

    看不明白可以看群里视频

    注意排序实现的具体方式,不要用局部变量,否则占空间太多,和空间复杂度不符。

    好,我们开始。

    • 选择排序

    选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在待排序序列的起始位置,直到全部待排序的数据元素排完。时间复杂度O(N^2)

    for i in range(len(l)):#意义是第i个位置开始挑第i大(小)的元素
        for j in range(i,len(l)):#和其他待排序的元素比较
    	if l[j]<l[i]:#更大就交换
    	    l[j],l[i]=l[i],l[j]
    • 冒泡排序

    冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法

    它重复地走访过要排序的元素列,一次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换(一般进行n次即可,第n次一定会把第n小的元素放到正确位置)。

    这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。时间复杂度O(N^2)

    for i in range(len(l)-1):#下标和i无关,代表的只是第i次排序,最多需要len(l)-1次排序即可
        for j in range(len(l)-1):#遍历每一个元素
    	if l[j]<l[j+1]:#本元素比下一个元素小,就交换
    		l[j],l[j+1]=l[j+1],l[j]

     分析一下其实每次排序都会多一个元素已经确定了位置,不需要再次遍历。

    所以j循环可以改成len(l)-i-1

    时间复杂度没变。

     

    • 插入排序

    有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。

    for i in range(1,len(l)):#意义是第i个元素开始插入i之前的序列(已经有序)
        for j in range(i,0,-1):#只要比它之前的元素小就交换
    	if l[j]<l[j-1]:
    	    l[j],l[j-1]=l[j-1],l[j]
    	else:
                break#直到比前一个元素大

     

    • 归并排序

    速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列

    试想:假设已经有两个有序数列,分别存放在两个数组s,r中,我们如何把他们有序的合并在一起?

    归并排序就是在重复这样的过程,首先单个元素合并为含有两个元素的数组(有序),然后这种数组再和同类数组合并为四元素数组,以此类推,直到整个数组合并完毕。

    def gg(l,ll):#合并函数
        a,b=0,0
        k=[]#用来合并的列表
        while a<len(l) and b<len(ll):#两边都非空
            if l[a]<ll[b]:
                k.append(l[a])
                a=a+1
            elif l[a]==ll[b]:a=a+1#实现去重
            else:
                k.append(ll[b])
                b=b+1
        k=k+l[a:]+ll[b:]#加上剩下的
        return k
    
    def kk(p):#分到只剩一个元素就开始合并
        if len(p)<=1:
            return p
        a=kk(p[0:len(p)//2])#不止一个元素就切片
        b=kk(p[len(p)//2:])
        return gg(a,b)#返回排好序的一部分
    l=list(map(int,input().split(" ")))
    print(kk(l))
    • 快速排序

    快速排序(Quicksort)是对冒泡排序的一种改进。

    快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

    • 随机化快排

    快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在数组已经有序的情况下,每次划分将得到最坏的结果。比如1 2 3 4 5,每次取第一个元素,就退化为了O(N^2)。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。

    这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度

    进一步提升可以分割为三部分,即小于区,等于区,大于区,减小了递归规模,并克服了多元素相同的退化。

    def gg(a,b):
        global l
        if a>=b:#注意停止条件,我以前没加>卡了半小时
            return
        x,y=a,b
        import random#为了避免遇到基本有序序列退化,随机选点
        g=random.randint(a,b)
        l[g],l[y]=l[y],l[g]#交换选中元素和末尾元素
        while a<b:
            if l[a]>l[y]:#比目标元素大
                l[a],l[b-1]=l[b-1],l[a]#交换
                b=b-1#大于区扩大
                #注意:换过以后a不要加,因为新换过来的元素并没有判断过
            else:a=a+1#小于区扩大
        l[y],l[a]=l[a],l[y]#这时a=b
        #现在解释a和b:a的意义是小于区下一个元素
        #b是大于区的第一个元素
        gg(x,a-1)#左右分别递归
        gg(a+1,y)
    
    l=list(map(int,input().split(" ")))
    gg(0,len(l)-1)
    print(l)
    
    • 堆排序

    堆排序(HeapSort)是一树形选择排序。堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)的记录。

    由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

    堆排序是就地排序,辅助空间为O(1).

    它是不稳定的排序方法。

    主要思想:维持一个大根堆(根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆,又称最大堆。注意:①堆中任一子树亦是堆。②以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定义k叉堆。)

    第一步:通过调整原地建立大根堆

    第二步:每次交换堆顶和边界元素,并减枝,然后调整堆顶下沉到正确位置。

    def down(i,k):#在表l里的第i元素调整,k为边界
    
    #优先队列也是通过这种方式实现的
        global l
        while 2*i+2<k:#右孩子不越界
            lift,right=2*i+1,2*i+2
            m=max(l[i],l[lift],l[right])
            if m==l[i]:#不需要调
                break
            if m==l[lift]:#把最大的换上来
                l[i],l[lift]=l[lift],l[i]
                i=lift#目的节点下标更新
            else:#把最大的换上来
                l[i],l[right]=l[right],l[i]
                i=right#目的节点下标更新
        if 2*i+1<k:#判断左孩子
            if l[2*i+1]>l[i]:
                l[i],l[2*i+1]=l[2*i+1],l[i]
    
    def main():
        global l
        for j in range(1,len(l)+1):#调大根堆
            i=len(l)-j
            down(i,len(l))
        for i in range(len(l)-1,-1,-1):#排序
            l[i],l[0]=l[0],l[i]#最大和边界交换,剪枝
            down(0,i)
        print(l)
        
    l=list(map(int,input().split(" ")))
    main()
    
            
        
    
            
    
    • 桶排序

    桶排序不是基于比较的排序方法,只需对号入座。将相应的数字放进相应编号的桶即可。

    当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间o(n)

    对于海量有范围数据十分适合,比如全国高考成绩排序,公司年龄排序等等。

    l=list(map(int,input().split(" ")))
    n=max(l)-min(l)
    p=[0]*(n+1)#为了省空间
    for i in l:
        p[i-min(l)]=1#去重排序,做标记即可
    for i in range(n):
        if p[i]==1:#判断是否出现过
            print(i+min(l),end=" ")
    • 希尔排序

    希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。

    通过缩小有序步长来实现。

     

    def shell(arr):
     n=len(arr)#初始化步长
     h=1
     while h<n/3:
      h=3*h+1
     while h>=1:#判断,退出后就有序了。
      for i in range(h,n):
       j=i
       while j>=h and arr[j]<arr[j-h]:#判断是否交换
        arr[j], arr[j-h] = arr[j-h], arr[j]
        j-=h
      h=h//3#逐渐缩小步长
     print arr

    稳定性及时间复杂度

    排序稳定性概念:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

    时间复杂度:时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。可以理解为和常数操作所成的一种关系(常数操作为O(1))

    空间复杂度类似。

    下面给出各类排序的对比图:

     

     

     

    • 基数排序

    因为桶排序是稳定的,基数排序就是很多次桶排序而已,按位进行桶排序即可。

    (个人认为桶排序名字不恰当,因为桶是先进后出,和稳定的算法正好反了,)

     

     

     

     

    总:

    比较排序和非比较排序

          常见的排序算法都是比较排序,非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置。

          比较排序的时间复杂度通常为O(n2)或者O(nlogn),比较排序的时间复杂度下界就是O(nlogn),而非比较排序的时间复杂度可以达到O(n),但是都需要额外的空间开销。

    • 若n较小(数据规模较小),插入排序或选择排序较好
    • 若数据初始状态基本有序(正序),插入、冒泡或随机快速排序为宜
    • 若n较大,则采用时间复杂度为O(nlogn)的排序方法:快速排序或堆排序
    • 快速排序是目前基于比较的排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
    • 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。

     

     

     

     

     

     

     

     

     

    展开全文
  • 排序算法想必大家陌生,今天就来详细的做个总结,包括排序算法的复杂度,稳定性,实现方式。
  • 第1章习题 ...数据分析是一个目的地收集和整合数据的过程。( ) Python是一门胶水语言,可以轻松地操作其它语言编写的库。( ) 如果要卸载指定环境中的包,则直接使用remove命令移除即可。...
  • 【Python数据分析与可视化】期末复习试题集

    万次阅读 多人点赞 2021-12-03 22:08:06
    1.Jupyter notebook具备的功能是 () Jupyter notebook可以直接生成一份交互式文档 Jupyter notebook可以安装Python库 Jupyter notebook可以导出HTML文件 Jupyter notebook可以将文件分享给他人 right_answers:...
  • 合并排序算法排序过程 每个程序员都需要了解他们的算法和数据结构。 在研究它们时,您需要确保确切了解它的功能,时间和空间的复杂性以及采用这种方式的原因,并且不仅能够对其进行编码,而且能够手动执行。 这就是...
  • #include <stdio.h> #include <stdbool.h> //创建单链表的成员 struct single_list { struct single_list *... //数据域 }; //创建单链表 struct single_list_head { struct single_list *head...
  • 基于比较排序排序方法,其复杂度无法突破nlognnlog⁡nn\log{n} 的下限,但是 计数排序排序 和基数排序是分布排序,他们是可以突破这个下限达到O(n)的的复杂度的。 1. 计数排序 概念 计数排序是一种稳定的...
  • 408数据结构

    千次阅读 多人点赞 2020-04-30 15:32:49
    数据对象是具有相同性质的数据元素的集合,是数据的一个子集。 数据类型是一个值的集合和定义在此集合上一组操作的总称。 原子类型:其值可再分的数据类型 结构类型:其值可以再分解为若干成分(分...
  • 数据结构课设】家谱管理系统(内附源码)

    千次阅读 多人点赞 2022-04-25 16:26:37
    实现具有下列功能的家谱管理系统 【功能要求】 (1)输入文件以存放最初家谱中各成员的信息,成员的信息中均应包含以下内容:姓名、出生日期、婚否、地址、健在否、死亡日期(若其已死亡),......
  • 根据此书所做随笔笔记。 一、绪论 1.1、数据机构的研究内容 ...由于数据必须在计算机中处理,因此能局限于数据本身的数学问题的研究,还必须考虑数据的物理结构,即数据在计算机中的存储结构。 1.
  • 数据挖掘大学练习题

    千次阅读 2021-01-07 13:35:03
    数据挖掘中北大学练习题 第一章 绪论 一.判断题(共10题,83.0分) 下面各项是否是数据挖掘任务? 1根据性别划分公司的顾客 我的答案:× 得分: 8.3分正确答案:× 答案解析: 这属于简单的数据库查询 2根据可盈利性...
  • ④ 【答案】D 【解析】并查集是一种树形的数据结构,顾名思义,它用于处理一些交集的 合并 及 查询 问题。查找(Find):确定某个元素处于哪个子集;合并(Union):将两个子集合并成一个集合。并查集的优化方法...
  • 看了CSDN上不少关于EXCEL函数介绍的,但都没有结合动图来介绍。而本篇博客通过录制动图的方法,并且结合EXCEL官网对于函数的介绍,让大家更直观地感受到函数的具体功能究竟如何。当然,如果你本身只是想了解一下...
  • 数据挖掘】期末复习模拟题(暨考试题)

    千次阅读 多人点赞 2021-12-15 16:29:11
    Python数据挖掘Python数据挖掘单选题多选题判断题程序填空程序分析 Python数据挖掘 单选题 什么是KDD ( ) ? A、数据挖掘与知识发现 B、领域知识发现 C、文档知识发现 D、态知识发现 人从出生到长大的过程中,是...
  • 数据结构试卷(一)

    千次阅读 多人点赞 2020-09-05 18:48:56
    一、单选题(每题 2 分,共20分) 栈和队列的共同特点是( A )。 A....B....C....D....用链接方式存储的队列,在进行插入运算时( D ). A....B....C....D....以下数据结构中哪一个是非线性结构?...设一个二维数组A[m][n],假设A[0][0]
  • (在此仅展示题目,所有数据、代码、答案、习题等点我头像,在资源中!!!) 以下关于pandas 数据预处理说法正确的是()。 A、 pandas没有做哑变量的函数 B、 在导入其他厍的情况下,仅仅使用 ...
  • 依次得到的各最短路径的目标顶点是:A A.6, 7, 5, 3, 2, 4 B.6, 2, 5, 7, 3, 4 C.2, 3, 4, 5, 6, 7 D.2, 4, 3, 6, 5, 7 5-1下列代码的功能是对一个给定的图G执行拓扑排序,其中TopNum[]从1开始记录拓扑序。...
  • 数据结构】八种常见数据结构介绍

    万次阅读 多人点赞 2021-02-05 13:59:44
    数据结构是计算机存储、组织数据的方式。一种好的数据结构可以带来更高的运行或者存储效率。数据在内存中是呈线性排列的,但是我们可以使用指针等道具,构造出类似“树形”的复杂结构。下面介绍八个常见的数据结构。
  • 数据结构快速掌握和温习-面试神器

    万次阅读 2021-08-09 21:27:56
    由于原文部分文字没有显示,本文有所修改。主要包括文字和缩进。 目录 Q1:数据结构和算法的知识点整理: Q2:链表,队列和栈的区别 Q3:简述快速排序过程 Q4:快速排序算法的...Q10:排序有哪些分类? Q...
  • 1. 某超市研究销售纪录数据后发现,买啤酒的人很大概率也会购买尿布,这种属于数据挖掘的哪类问题?(A) A. 关联规则发现 B. 聚类 C. 分类 D. 自然语言处理 2. 以下两种描述分别对应哪两种对...
  • 从今天开始,我将正式开启一个新的打卡专题——【数据结构·水滴计划】,没错!这是今年上半年的一整个系列计划!...同时还配专门的笔记总结和文档教程哦!想要搞定,搞透数据结构的同学,欢迎订阅本专栏!.........
  • 数据挖掘150道笔试题

    千次阅读 2020-11-29 01:24:53
    作者:白宁超2016年10月16日13:44:06摘要:正值找工作之际,数据挖掘150道面试题涵盖很多基础知识点,如果你针对求职提前针对性准备,可以以此为为参照检查自己水平,如果你为求职,也可以针对这些基础佐以巩固,...
  • 选择法排序是指每次选择索要排序的数组中的最小值(这里是由小到大排序,如果是由大到小排序则需要选择最大值)的数组元素,将这些数组元素的值与前面没有进行排序的数组元素值进行互换 代码实现需要注意的是:声明...
  • 1-2将N个数据按照从小到大顺序组织存放在一个单向链表中。如果采用二分查找,那么查找的平均时间复杂度是O(logN)。 F 1-3在一棵二叉搜索树上查找63,序列39、101、25、80、70、59、63是一种可能的查找时的结点值比较...
  • ​ 空值一般表示数据未知、适用或将在以后添加数据。缺失值是指数据集中某个或某些属性的值是完整的。 ​ 一般空值使用None表示,缺失值使用NaN表示 使用isnull()和notnull()函数 ​ 可以判断数据集中是否...
  • 数据来源:数据分析专项练习题库 单选题 共29题,共58.0分 1.以下哪个属于关系型数据库? A.MySQLB.MongoDB C.Oracle D.PostgreSQL 2.以下哪个SQL函数可以完成对数字的四舍五入( )。 A.floor B.ceilingC....
  • 【Data Mining】【第一章作业】

    千次阅读 2022-04-05 17:00:07
    1.下列属于数据挖掘任务的是( ) A、根据性别划分公司的顾客 B、计算公司的总销售额 C、预测一对骰子的结果 D、利用历史记录预测公司的未来股价 正确答案: D 2.下述四种方法哪一种不是常见的分类方法( ) A、决策树 B...

空空如也

空空如也

1