第3章 线性表

第3章 线性表

线性表( List ):零个或多个数据元素的有限序列。
如果用数学语言来进行定义。可如下:
若将线性表记为(a1,…,a1,ai,a+1,…,an),则表中a-1领先于a,a领先于 ai+1,称a1是 a的直接前驱元素,a+1是a的直接后继元素。当i=1,2,…,n-1时,a有且仅有一个直接后继,当i=2,3,…, n时,a有且仅有一个直接前驱。如图3-2-1所示。

在这里插入图片描述

在较复杂的线性表中,一个数据元素可以由若干个数据项组成。

3.1线性表的抽象数据类型

线性表的抽象数据类型定义如下:

ADT 线性表(List)
Data
	线性表的数据对象集合为{a,a2.. an),每个元素的类型均为DataType。其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元章有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。
Operation
	InitList(*L):初始化操作,建立一个空的线性表L。
	ListEmpty(L):若线性表为空,返回true,否则返回false。
    ClearList(*L):将线性表清空。
    GetElem (L,i,*e):将线性表I中的第i个位置元素值返回给e。
    LocateElem(L,e):在线性表I中查找与给定值e相等的元素,如果查找成功,返回
    该元素在表中序号表示成功;否则,返回0表示失败。
    ListInsert ( *L,i,e):在线性表I中的第i个位置插入新元素e。
    ListDelete (*L,i,*e):删除线性表I中第i个位置元素,并用e返回其值。	
    ListLength(L):返回线性表I的元素个数。
endADT

比如,要实现两个线性表集合A和B的并集操作。即要使得集合A=AUB。说白了,就是把存在集合B中但并不存在A中的数据元素插入到A中即可。
仔细分析一下这个操作,发现我们只要循环集合B中的每个元素,判断当前元素是否存在A中,若不存在,则插入到A中即可。思路应该是很容易想到的。
我们假设La表示集合A,Lb表示集合B,则实现的代码如下:

/*将所有的在线性表Lb中但不在La中的数据元素插入到La中*/
void union (List *La, List Lb){
	int La_len, Lb_len,i;
    ElemType e; /*声明与La和Lb相同的数据元素e*/
    La_len = ListLength (La) ;/*求线性表的长度*/
    Lb_len = ListLength (Lb);
    for (i=1; i<=Lb_len; i++){
        GetElem (Lb, i,e);/*取Lb中第i个数据元素赋给e*/
        if( !LocateElem(La,e, equal))/*La中不存在和e相同数据元素*/
        	ListInsert( La, +tLa_len,e):/*插入*/
    }
}

3.2线性表的顺序存诸结构

3.2.1顺序存储定义

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

3.2.2顺序存储方式

来看线性表的顺序存储的结构代码。

#define MAXSIZE 20
/*存储空间初始分配量*/
typedef int ElemType;
/*ElemType类型根据实际情况而定,这里假设为int*/
typedef struct{
    ElemType data [MAXSIZE];/*数组存储数据元素,最大值为MAXSIZE*/
    int length; /*线性表当前长度*/
}SqList;

这里,我们就发现描述顺序存储结构需要三个属性:
存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。线性表的最大存储容量:数组长度MaxSize。线性表的当前长度:length。

3.2.3数据长度与线性表长度区别

在任意时刻,线性表的长度应该小于等于数组的长度。

3.2.4地址计算方法

由于我们数数都是从1开始数的,线性表的定义也不能免俗,起始也是1,可C语言中的数组却是从0开始第一个下标的,于是线性表的第i个元素是要存储在数组下标为i-1的位置,即数据元素的序号和存放它的数组下标之间存在对应关系。

存储器中的每个存储单元都有自己的编号,这个编号称为地址。由于每个数据元素,不管它是整型、实型还是字符型,它都是需要占用一定的存储单元空间的。假设占用的是c个存储单元,那么线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数)。

LOC(a(i+1)= LOC(a)+c
所以对于第i个数据元素a的存储位置可以由a推算得出:
LOC(ai)=LOC(a)+(i-1)*c
在这里插入图片描述

3.3顺序存诸结构的插入与删除

3.3.1获得元素操作

对于线性表的顺序存储结构来说,如果我们要实现 GetEem操作,即将线性表L中的第i个位置元素值返回,其实是非常简单的。就程序而言,只要i的数值在数组下标范围内,就是把数组第i-1下标的值返回即可。来看代码:

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int status;
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
/*初始条件:顺序线性表I已存在,1≤i≤ListLength (L)*/
/*操作结果:用e返回L中第i个数据元素的值*/
Status GetElem(sqList L,int i,ElemType *e){
    if(L.length==0 || i<1 || i>L.length)
		return ERROR;
	*e=L.data[i-1];
    return OK;
}

注意这里返回值类型 Status是一个整型,返回OK代表1,ERROR代表0。之后代码中出现就不再详述。

3.3.2插入操作

插入算法的思路:

  1. 如果插入位置不合理,抛出异常;

  2. 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;

  3. 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位
    置;

  4. 将要插入元素填入位置i处;

  5. 表长加1。

    实现代码如下:

/*初始条件:顺序线性表L已存在,1≤i≤ListLength (L),*/
/*操作结果:在工中第i个位置之前插入新的数据元素e,L的长度加1*/
Status ListInsert ( sqList *L,int i,ElemType e){
    int k;
    if(L->length==MAXSIZE)/*顺序线性表已经满*/
    	return ERROR;
    if(i<1 || i>L->length+1)/*当i不在范围内时*/
        return ERROR;
    if(i<=L->length){/*若插入数据位置不在表尾*/
        for (k=L->length-1;k>=i-1; k--) /*将要插入位置后数据元素向后移动一位*/
        	L->data[k+1]=L->data[k];
    }
    L->data[i-1]=e;/*将新元素插入*/
    L->length++;
    return OK; 
}

3.3.3 删除操作

删除算法的思路:

  1. 如果删除位置不合理,抛出异常;
  2. 取出删除元素;
  3. 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
  4. 表长减1。

实现代码如下:

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除工的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete(SqList *L, int i,ElemType *e){
    int k;
    if(L->length==0) /*线性表为空*/
    	return ERROR;
    if(i<1 || i>L->length+1)/*当i不在范围内时*/
        return ERROR;
    *e=L->data[i-1];
    if(i<=L->length){/*若数据位置不在表尾*/
        for (k=i;k<L->length; k++) /*将删除位置后数据元素向前移动一位*/
                L->data[k-1]=L->data[k];
    }
    L->length++;
    return e
}

3.3.4线性表顺序存储结构的优缺点

在这里插入图片描述

3.4线性表的链式存储结构

3.4.1顺序存储结构不足的解决办法

前面我们讲的线性表的顺序存储结构。它是有缺点的,最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。能不能想办法解决呢?

3.4.2线性表链式存储结构定义

任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置。
因此,为了表示每个数据元素a与其直接后继数据元素as1之间的逻辑关系,对数据元素a来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
n个结点(a的存储映像)链结成一个链表,即为线性表(a1,az,…, an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。

对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了

有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点头结点的数据域可以不存储任何信息,谁叫它是第一个呢
在这里插入图片描述

3.4.3头指针与头结点的异同

头指针与头结点的异同点,如图所示
在这里插入图片描述

3.4.4线性表链式存储结构代码描述

单链表中,我们在C语言中可用结构指针来描述。

/*线性表的单链表存储结构*/
typedef struct Node{
    ElemType data;
	struct Node *next;
} Node;
typedef struct Node *LinkList;/*定义 LinkList*/

3.5单链表的读取

获得链表第i个数据的算法思路:
1.声明一个结点p指向链表第一个结点,初始化j从1开始;

2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;

3.若到链表末尾p为空,则说明第i个元素不存在;

4.否则查找成功,返回结点p的数据。

实现代码算法如下:

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*//*操作结果:用e返回I中第主个数据元素的值*/
Status GetElem(LinkList L, int i,ElemType *e){
    int j;
    LinkList p; /*声明一结点p*/
    p=L->next;  /*让p指向链表I的第一个结点*/
    j=1;  /*为计数器*/
    while (p && j<i){  /*p不为空或者计数器j还没有等于i时,循环继续*/
    	p=p->next;/*让p指向下一个结点*/	
        ++j;
    }
    if( !p || j>i)
   		return ERROR;/*第i个元素不存在*/
    *e = p->data;   /*取第i个元素的数据*/
    return OK;
}

3.6单链表的插入与删除

3.6.1 单链表的插入

单链表第i个数据插入结点的算法思路:

  1. 声明一结点p指向链表第一个结点,初始化j从1开始;

  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;

  3. 若到链表末尾p为空,则说明第i个元素不存在;

  4. 否则查找成功,在系统中生成一个空结点s;

  5. 将数据元素e赋值给s->data;

  6. 单链表的插入标准语句s->next=p->nextp->next=s;

  7. 返回成功。

实现代码算法如下:

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L),*/
/*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
status ListInsert (LinkList *L, int i,ElemType e){
    int j;
    LinkList p,s;
    p=*L;
    j= 1;
    while (p&&j<i){ /*寻找第i个结点*/
    	p = p->next;
        j++;
    }
    if( !p || j>i)
   		return ERROR;/*第i个元素不存在*/
    s= (LinkList) malloc (sizeof (Node));/*生成新结点(C标准函数)*/
    s->data =e;
	s->next =p->next; /*将p的后继结点赋值给s的后继*/
	p->next =s; /*将s赋值给p的后继*/
	return OK;
}

3.6.2单链表的删除

单链表第i个数据删除结点的算法思路:

  1. 声明一结点p指向链表第一个结点,初始化j从1开始;

  2. 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加

  3. 若到链表末尾p为空,则说明第i个元素不存在;

  4. 否则查找成功,将欲删除的结点p->next赋值给q;

  5. 单链表的删除标准语句p->next=q->next;

  6. 将q结点中的数据赋值给e,作为返回;

  7. 释放q结点;

  8. 返回成功。

    实现代码算法如下:

/*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
/*操作结果:删除工的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete ( LinkList *L, int i,ElemType *e){
    int j;
	LinkList p,q;
    p=*L;
    j= 1;
    while (p&&j<i){ /*寻找第i个结点*/
    	p = p->next;
        j++;
    }
    if( !p || j>i)
   		return ERROR;/*第i个元素不存在*/
    q=p->next;
    p->next -q->next;/*将q的后继赋值给p的后继*/
	*e - q->data;/*将q结点中的数据给e*/
	free (q);/*让系统回收此结点,释放内存*/
	return OK;
}

3.7单链表的整表创建*

单链表整表创建的算法思路:

  1. 声明一结点p和计数器变量i;

  2. 初始化一空链表L;

  3. 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;

  4. 循环:

    • 生成一新结点赋值给p;
    • 随机生成一数字赋值给p的数据域p->data;
    • 将p插入到头结点与前一新结点之间。
      实现代码算法如下:
/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead (LinkList *E, int n){
    LinkList p;int i;
    srand(time (0)); /*初始化随机数种子*/
    *L=(LinkList) malloc (sizeof (Node));
    (*L) ->next = NULL;  /*先建立一个带头结点的单链表*/
    for (i=0; i<n; i++){
        p=( LinkList ) malloc (sizeof (Node));/*生成新结点*/
        p->data=rand() %100+1;  /*随机生成100以内的数字*/
        p->next =(*L)->next;
        (*L) ->next= p; /*插入到表头*/
    }   
}

可事实上,我们还是可以不这样干,为什么不把新结点都放到最后呢,这才是排队时的正常思维,所谓的先来后到。我们把每次新结点都插在终端结点的后面,这种算法称之为尾插法
实现代码算法如下:

/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail(LinkList *L, int n){
    LinkList p,r;
    int i;
    srand(time (0));   /*初始化随机数种子*/
    *L=(LinkList) malloc (sizeof (Node));/*为整个线性表*/
    r=*L; /*r为指向尾部的结点*/
    for (i=0; i<n; i++){
        p=( LinkList ) malloc (sizeof (Node));/*生成新结点*/
        p->data=rand() %100+1;  /*随机生成100以内的数字*/
        r->next=p; /*将表尾终端结点的指针指向新结点*/
		r=p; /*将当前的新结点定义为表尾终端结点*/	        
    }   
    r->next= NULL; /*表示当前链表结束*/
}

3.8单链表的整表删除

单链表整表删除的算法思路如下:

  1. 声明一结点p和q;
  2. 将第一个结点赋值给p;
  3. 循环:
  • 将下一结点赋值给q;释放p;
  • 将q赋值给p。
    实现代码算法如下:
/*初始条件:顺序线性表L已存在,操作结果:将L重置为空表*/
Status ClearList(LinkList *L){
    LinkList p,q;
    p=(*L)->next; /*p指向第一个结点*/
    while(p){/*没到表尾*/
        q=p->next;
        free(p);
        p=q;
    }      
    (*L)->next=NULL; /*头结点指针域为空*/
    return OK;
}

3.9单链表结构与顺序存储结构优缺点

在这里插入图片描述

3.10静态链表

一些语言,如Basic、Fortran等早期的编程高级语言,由于没有指针,链表结构按照前面我们的讲法,它就没法实现了。怎么办呢?
有人就想出来用数组来代替指针,来描述单链表。真是不得不佩服他们的智慧,我们来看看他是怎么做到的。
首先我们让数组的元素都是由两个数据域组成,data和cur。也就是说,数组的每个下标都对应一个 data和一个cur。数据域data,用来存放数据元素,也就是通常我们要处理的数据;而游标cur相当于单链表中的next指针,存放该元素的后继在数组中的下标。
我们把这种用数组描述的链表叫做静态链表,这种描述方法还有起名叫做游标实现法
为了我们方便插入数据,我们通常会把数组建立得大一些,以便有一些空闲空间可以便于插入时不至于溢出。

/*线性表的静态链表存储结构*/
#define MAXSIZE 1000/*假设链表的最大长度是1000*/
typedef struct{
    ElemType data;
    int cur; /*游标(cursor),为0时表示无指向*/
} Component,StaticLinkList[MAXSIZE]; 

在这里插入图片描述

此时的图示相当于初始化的数组状态,见下面代码:

/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space)  {
	int i;
	for (i=0; i<MAXSIZE-1; i++)  
		space[i].cur = i+1;
	space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
	return OK;
}

在这里插入图片描述

3.10.1静态链表的插入操作

静态链表中要解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。
我们前面说过,在动态链表中,结点的申请和释放分别借用malloc()和free()两个函数来实现。在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,所以我们需要自己实现这两个函数,才可以做插入和删除的操作。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space) { 
	int i = space[0].cur; /* 当前数组第一个元素的cur存的值 */
	                     /* 就是要返回的第一个备用空闲的下标 */
	if (space[0]. cur)         
	    space[0]. cur = space[i].cur;  /* 由于要拿出一个分量来使用了,所以我们就得把它的下一个分量用来做备用 */
	return i;
}

在这里插入图片描述

/*  在L中第i个元素之前插入新的数据元素e   */
Status ListInsert(StaticLinkList L, int i, ElemType e)   {  
    int j, k, l;   
    k = MAXSIZE - 1;   /* 注意k首先是最后一个元素的下标 */
    if (i < 1 || i > ListLength(L) + 1)   
        return ERROR;   
    j = Malloc_SSL(L);   /* 获得空闲分量的下标 */
    if (j) {   
		L[j].data = e;   /* 将数据赋值给此分量的data */
		for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
		   k = L[k].cur;           
		L[j].cur = L[k].cur;/* 把第i个元素之前的cur赋值给新元素的cur */
		L[k].cur = j;  /* 把新元素的下标赋值给第i个元素之前元素的ur */
		return OK;   
    }   
    return ERROR;   
}

3.10.2静态链表的删除操作

和前面一样,删除元素时,原来是需要释放结点的函数free ()。现在我们也得自己实现它:

/*  将下标为k的空闲结点回收到备用链表 */
void Free_SSL(StaticLinkList space, int k) {  
    space[k].cur = space[0].cur; /* 把第一个元素的cur值赋给要删除的分量cur */
    space[0].cur = k;   /* 把要删除的分量下标赋值给第一个元素的cur */
}

/*  删除在L中第i个数据元素   */
Status ListDelete(StaticLinkList L, int i)   { 
    int j, k;   
    if (i < 1 || i > ListLength(L))   
        return ERROR;   
    k = MAXSIZE - 1;   
    for (j = 1; j <= i - 1; j++)   
        k = L[k].cur;   
    j = L[k].cur;   
    L[k].cur = L[j].cur;   
    Free_SSL(L, j);   
    return OK;   
} 

在这里插入图片描述

/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L){
    int j=0;
    int i=L[MAXSIZE-1].cur;
    while(i){
        i=L[i].cur;
        j++;
    }
    return j;
}

3.10.3静态链表优缺点

在这里插入图片描述

总的来说,静态链表其实是为了给没有指针的高级语言设计的一种实现单链表能力的方法。尽管大家不一定会用得上,但这样的思考方式是非常巧妙的,应该理解其思想,以备不时之需。

3.11循环链表

将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circuar linkedlist)。

在这里插入图片描述

其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p->next不等于头结点,则循环未结束。
在单链表中,我们有了头结点时,我们可以用O(1)的时间访问第一个结点,但对于要访问到最后一个结点,却需要O(n)时间,因为我们需要将单链表全部扫描一遍。
有没有可能用O(1)的时间由链表指针访问到最后一个结点呢?当然可以。
不过我们需要改造一下这个循环链表,不用头指针,而是用指向终端结点的尾指针来表示循环链表(如图3-13-5所示),此时查找开始结点和终端结点都很方便了。

在这里插入图片描述

从上图中可以看到,终端结点用尾指针rear 指示,则查找终端结点是0[1),而开始结点,其实就是rear->next->next,其时间复杂也为O(1)。
举个程序的例子,要将两个循环链表合并成一个表时,有了尾指针就非常简单了。比如下面的这两个循环链表,它们的尾指针分别是 rearA 和 rearB,要想把它们合并,只需要如下的操作即可,如图3-13-7所示。
在这里插入图片描述

p=rearA->next;   			    /* 保存A表的头结点,即① */
rearA->next=rearB->next->next;	/* 将本是指向B表的第一个结点(不是头结点)*/
                 				/* 赋值给reaA->next,即② */
q=rearB->next;
rearB->next=p;	 	/* 将原A表的头结点赋值给rearB->next,即③ */
free(q);					   	/* 释放q */

3.12双向链表

我们在单链表中,有了next 指针,这就使得我们要查找下一结点的时间复杂度为O(1)。可是如果我们要查找的是上一结点的话,那最坏的时间复杂度就是O(n)了,因为我们每次都要从头开始遍历查找。
为了克服单向性这一缺点,我们的老科学家们,设计出了双向链表。双向链表(double linked list)是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。

/*线性表的双向链表存储结构*/
typedef struct DulNode{
    ElemType data;
    struct DuLNode *prior;    	/*直接前驱指针*/
    struct DuLNode *next;		/*直接后继指针*/
} DulNode, *DuLinkList;

在这里插入图片描述

我们现在假设存储元素e的结点为s,要实现将结点s插入到结点p和p -> next之间需要下面几步,如图3-14-5所示。

在这里插入图片描述

s - >prior = p;   			/*把p赋值给s的前驱,如图中①*/
s -> next = p -> next;		/*把p->next赋值给s的后继,如图中②*/
p -> next -> prior = s;		/*把s赋值给p->next的前驱,如图中③*/
p -> next = s;				/*把s赋值给p的后继,如图中④*/

若要删除结点p,只需要下面两步骤,如图3-14-6所示。
在这里插入图片描述

p->prior->next=p->next; /*把p->next赋值给p->prior的后继,如图中①*/
p->next->prior=p->prior; /*把p->prior赋值给p->next的前驱,如图中②*/
free(p);					/*释放结点*/

3.13总结回顾

在这里插入图片描述

链表算法小结

快慢指针

腾讯面试题题目:快速找到未知长度单链表的中间节点。
普通的方法很简单,首先遍历一遍单链表以确定单链表的长度L。然后再次从头节点出发循环L/2次找到单链表的中间节点。

算法复杂度为:O(L+L/2)=O(3L/2)。能否再优化一下这个时间复杂度呢?有一个很巧妙的方法:利用快慢指针!

/*利用快慢指针原理:设置两个指针*search,*mid都指向单链表的头节点。其中* search的移动速度是*mid的2倍。当*search指向末尾节点的时候,mid正好就在中间了。这也是标尺的思想。*/
status GetMidNode (LinkList L,ElemType *e){
	LinkList search, mid;
    mid = search = L;
	while (search->next !=NULL){
		// search移动的速度是mid 的2倍
    	if (search->next->next !=NULL){
			search = search->next->next;
            mid = mid->next;
		} else {
			search =search->next;
		}
	}
	*e = mid->data;
	return OK;
}
约瑟夫问题

据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。
然而Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

//n个人围圈报数,报m出列,最后剩下的是几号?
#include <stdio.h>
#include <stdlib.h>
typedef struct node{
	int data;
	struct node *next;
}node;
node *create(int n){
	int i = 1;
	node *p = NULL, *head;
	node *s;//纯C环境 比较老的版本,所以要求变量定义只能在开头,否则illegal use of this type as an expression
	head =(node*)malloc(sizeof(node));
	p= head;
	if(0 != n ){
		while ( i <=n ){
			s=(node*)malloc(sizeof(node));
			s->data = i++;//为循环链表初始化,第一个结点为1,第二个结点为2。
			p->next =s;
			p = s;
		}
		s->next =head->next;
	}
	free(head);
	return s->next;
}
int main(){
	int n=41;
	int m= 3;
	int i;
	node *p = create(n);
	node *temp;
	m%= n;
	while (p != p->next ){
		for (i =1; i < m-1; i++){
			p = p->next ;
		}
		printf("%d->", p->next->data);
		temp = p->next;//删除第m个节点
		p->next = temp->next ;
		free(temp);
		p = p->next ;
	}
	printf("%d\n", p->data );
	return 0;
}

在这里插入图片描述

判断单链表中是否有环

在这里插入图片描述

方法一:使用p、q两个指针,p总是向前走,但q每次都从头开始走,对于每个节点,看p走的步数是否和q一样。如图,当p从6走到3时,用了6步,此时若q从head出发,则只需两步就到3,因而步数不等,出现矛盾,存在环。
方法二:使用p、q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p== q,则存在环。

#include "stdio.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;/*status是函数的类型,其值是函数结果状态代码,如ok等*/
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
typedef struct Node{
    ElemType data;
	struct Node *next;
}Node ,*LinkList;

/*初始化带头结点的空链表*/
Status InitList (LinkList *L){
	*L=(LinkList)malloc(sizeof (Node) ) ;/*产生头结点,并使L指向此头结点*/
    if(! (*L)){/*存储分配失败*/        
		return ERROR;
    }
	(*L)->next=NULL;/*指针域为空*/
    return OK;
}
/*初始条件:顺序线性表L已存在。操作结果:返回L中数据元素个数*/
int ListLength (LinkList L){
    int i=0;
	LinkList p=L->next; /* p指向第一个结点*/
    while(p){
        i++;
		p=p->next;	
    }
	return i;
}
/*随机产生n个元素的值,建立带表头结点的单链线性表L(头插法))*/
void createListHead(LinkList *L,int n){
    LinkList p;
    int i; 
    srand(time(0)); /*初始化随机数种子*/
    *L= (LinkList ) malloc(sizeof (Node));
    (*L) ->next = NULL; /*建立一个带头结点的单链表*/
    for (i=0; i < n;i++){
        p = (LinkList)malloc(sizeof(Node)) ; /*生成新结点*/
        p->data = rand()%100+i;/*随机生成100以内的数字*/
        p->next =(*L)->next;
        ( *L)->next=p;/*插入到表头*/
    }
}
/*随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void createListTail(LinkList *L, int n){
    LinkList p,r;
    int i;
    srand(time (0));   /*初始化随机数种子*/
    *L=(LinkList) malloc (sizeof (Node));/*为整个线性表*/
    r=*L; /*r为指向尾部的结点*/
    for (i=0; i<n; i++){
        p=( LinkList ) malloc (sizeof (Node));/*生成新结点*/
        p->data=rand() %100+1;  /*随机生成100以内的数字*/
        r->next=p; /*将表尾终端结点的指针指向新结点*/
		r=p; /*将当前的新结点定义为表尾终端结点*/	        
    }   
    r->next= (*L)->next->next; 
}
/*比较步数的方法*/
int HasLoop1 (LinkList L){
    LinkList cur1 =L; //定义结点cur1
    int pos1= 0;  //cur1的步数
    while(cur1){ //cur1结点存在
        LinkList cur2=L;//定义结点cur2
        int pos2 =0;//cur2的步数
        while(cur2){//cur2结点不为空
            if(cur2== cur1){//当cur1与cur2到达相同结点时
                if(pos1==pos2){//走过的步数一样
                    break;//说明没有坏
                }else{ //否则
                    printf("环的位置在第&d个结点处。\n\n", pos2);
                    return 1;//有环并返回1
                }
            }
            cur2 =cur2->next;//如果没发现环,继续下—个结点
            pos2++;//cur2步数自增
        }
        cur1 =cur1->next;//cur1继续向后一个节点
        pos1++;//cur1步数自增
    } 
    return 0;
}
// 利用快慢指针的方法
int HasLoop2(LinkList L){
    LinkList p = L;		//慢指针p
    LinkList q = L;    //快指针q
    while (p != NULL && q != NULL && q->next != NULL){//结束的条件是1.没有环p或者q指向null 2.有环p==q,return结束循环
        p = p->next;	//下一个节点的地址							
        if (q->next != NULL)
            q = q->next->next;    //下下一个节点的地址
        printf("p:%d, q:%d \n", p->data, q->data);
        if (p == q)
            return 1;
    }
    return 0;
} 
int main (){    
    LinkList  L;
    Status i;
    char opp;
    ElemType e;
    int find;
    int temp;
    i =InitList(&L);
    printf("初始化L后:ListLength(L)=%d\n" , ListLength(L));
    printf(" \n1.创建有环链表(尾插法)\n2.创建无环链表(头插法)\n3.判断链表是否有环\n0.退出\n");
    while(opp != '0'){
        scanf( "%c",&opp);
        switch (opp){                        
            case '1':
                createListTail(&L,20);
                printf("成功创建有环L(尾插法)\n");
                printf ("\n");
                break;
            case '2':
                createListHead (&L,20);
                printf("成功创建无环L(头插法)\n");
                printf ( "\n");
                break;
            case '3':
                printf("方法一: \n\n");
                if(HasLoop1(L) ){
                    printf("结论:链表有环\n\n\n"); 
                } else{
					printf("结论:链表无环\n\n\n"); 
                }
                printf("方法二: \n\n");
                if(HasLoop2(L) ){
                    printf("结论:链表有环\n\n\n"); 
                } else{
					printf("结论:链表无环\n\n\n"); 
                }
                printf ("\n");
                break;
            case '0':
                exit(0);
        }
    }system("pause");
}
魔术师发牌问题

问题描述:魔术师利用一副牌中的13张黑牌,预先将他们排好后叠放在一起,牌面朝下。魔术师将最上面的那张牌数为1,把他翻过来正好是黑桃A,将黑桃A放在桌子上,第二次数1,2,将第一张牌放在这些牌的下面,将第二张牌翻过来,正好是黑桃2,也将它放在桌子上这样依次进行将13张牌全部翻出,准确无误。(循环链表实现)

#include <stdio.h>
#include <stdlib.h>
#define NUM 13;
typedef struct sqlList
{
    int data;
    struct sqlList *next;
} Node, *LinkList;

void init(LinkList *head)
{
    int i = NUM;
    *head =(LinkList)malloc(sizeof(Node));
    (*head)->data = 0;
    LinkList p, q = *head;
    while (--i)
    {
        p = (LinkList)malloc(sizeof(Node));
        (p)->data = 0;   
        (q)->next = p;
        q = p;     
    }
    (q)->next = *head; //最后指向头结点,构成循环链表
    // (**L).data = 1; //L是指向指针的指针,*L是指向结构体的指针 **L是结构体对象
}
void putCard(LinkList *L)
{
    int n = NUM;
    LinkList p = *L;
    for (int i = 1; i < n + 1; i++)
    {
        if (i != 1)
        {
            for (int j = 0; j < i; j++)
            {
                p = p->next;
                if (p->data != 0)
                {
                    j--;
                }
            }
        }
        p->data = i;
    }
}
void sendCard(LinkList *L)
{
    int i = NUM;
    LinkList p = NULL;
    while (i--)
    {
        if (p == NULL)
        {
            p = *L;
        }
        else
        {
            p = p->next;
        }
        printf("黑桃%d-->", (p)->data);
    }
}

void main()
{
    LinkList link;
    //初始化所有0
    init(&link);
    //放牌
    putCard(&link);
    //发牌
    sendCard(&link);
}

在这里插入图片描述

拉丁方阵问题

拉丁方阵是一种nxn的方阵,方阵中恰有n种不同的元素,每种元素恰有n个,并且每种元素在一行和一列中恰好出现一次。著名数学家和物理学家欧拉使用拉丁字母来作为拉丁方阵里元素的符号,拉丁方阵因此而得名。(循环链表实现)

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

typedef struct sqlList
{
    int data;
    struct sqlList *next;
} Node, *LinkList;

LinkList init(int n)
{
    LinkList head = NULL;
    LinkList p, q;
    for (int i = 0; i < n; i++)
    {
        p = (LinkList)malloc(sizeof(Node));
        p->data = i + 1;
        if (head == NULL)
        {
            head = p;
        }
        else
        {
            q->next = p;
        }
        q = p;
    }
    q->next = head;
    return head;
}

void show(LinkList *L, int n)
{
    LinkList p = *L;
    for (int j = 0; j < n; j++)
    {
        if (j > 0)
            p = p->next;

        for (int i = 0; i < n; i++)
        {
            printf("%d ", (p)->data);
            p = p->next;
        }
        printf("\n");
    }
}
void main()
{
    int n;
    printf("请输入拉丁方阵的行列数:");
    scanf("%d", &n);
    //初始化
    LinkList square = init(n);
    //打印
    show(&square, n);
}

在这里插入图片描述

双向循环链表题目

要求实现用户输入一个数使得26个字母的排列发生变化,例如用户输入3,输出结果:
DEFGHIJKLMNOPORSTUVWXYZABC
同时要支持负数,例如用户输入-3,输出结果:
XYZABCDEFGHIJKLMNOPQRSTUVW

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

typedef int ElemType;
typedef struct DulNode
{
    ElemType data;
    struct DulNode *next;
    struct DulNode *prior;
} Node, *LinkList;

void InitList(LinkList *L)
{
    LinkList p, q;
    int i;

    *L = (LinkList)malloc(sizeof(Node));
    (*L)->next = NULL;
    (*L)->prior = NULL;
    q = *L;
    for (i = 0; i < 26; i++)
    {
        p = (LinkList)malloc(sizeof(Node));
        p->data = 'A' + i;
        p->next = q->next;
        p->prior = q;
        q->next = p;
        q = p;
    }
    q->next = (*L)->next;
    (*L)->next->prior = q;
    (*L) = (*L)->next;
}
void show(ElemType n, LinkList *p)
{
    if (n > 0)
    {
        do
        {
            (*p) = (*p)->next;
        } while (--n);
    }
    if (n < 0)
    {
        do
        {
            (*p) = (*p)->prior;
        } while (++n);
    }
}
int main()
{
    LinkList L;
    ElemType n;
    printf("输入变化数字:");
    scanf("%d", &n);
    InitList(&L);
    show(n, &L);
    for (int i = 0; i < 26; i++)
    {
        printf("%c", L->data);
        L = L->next;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值