数据结构与算法
1、 算法的特征
1)有穷性:算法必须能在执行有限个步骤后终止
2)确定性:每条指令必须有确切的含义,相同的输入只能得出相同的输出
3)可行性:任何步骤都是可以被分解为基本的可执行步骤,即每个计算步骤都可以在有限时间内完成
4)输入:一个算法有0个或多个输出,所谓0个输入就是指算法本身定出了初始条件
5)输出:一个算法有一个或多个输出,以反应对输入数据加工后的结果,没有输出的算法是毫无意义的
2、算法的要求
1)正确性:能够正确地解决求解问题
2)可读性:算法应具有良好的可读性,以帮助人们理解
3)健壮性:输入非法数据时,算法能适当的作出反应或进行处理,而不会产生莫名的输出结果,程序也不会挂掉
4)高效率与低存储需求
算法复杂度
算法复杂性体现在该算法运行时占用计算机资源的多少上,计算机最重要的资源是时间(CPU)和空间(内存),因此复杂度可以分为时间和空间复杂度(性)。
语句总的执行次数T(n)是关于问题规模n的函数
时间复杂度O(n)最简单的描述是:计算机运行一个算法时,程序代码被执行的总次数 T(n) = O(f(n))
一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法
推导大O阶方法
- 用常数1取代运行时间中的所有加法常数
- 再修改后的运行次数函数中,只保留最高阶项
- 如果最高项存在且不是1,则去除与这个项相乘的常数
- 得到最后结果就是大O阶
时间复杂度分析:
只关注循环执行次数最多的一段代码
总复杂度等于最高阶项的复杂度
嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
空间复杂度S(n)是指算法消耗的内存空间,也是问题规模(需要处理的数据量)n的函数
递归函数的空间复杂度:递归函数嵌套的调用自己,函数的参数和局部变量占用内存空间在递去过程中会增长,在归来时候才逐层释放
递归
1、一个问题的解可以分解为几个子问题的解
2、这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。
3、存在基线/终止条件
例如:拆开俄罗斯套娃取到开箱钥匙的过程
伪代码实现过程:
void openBox(当前盒子)
{
打开盒子;
if(盒子里面是钥匙){
return钥匙;
}
else{
openBox(更小的盒子);
}
}
如果俄罗斯套娃的层数不确定
void openBox2(当前盒子,盒子的深度)
{
if(盒子的深度 >= 20)
return "我不干了";
if(盒子里是钥匙){
return 钥匙;
}
else{
盒子的深度加1;
openBox2(更小的盒子,盒子的深度);
}
}
线性表 List :由零个或多个数据元素组成的有限序列
线性表顺序存储结构的优点:
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中任意位置的元素
缺点:
- 插入和删除需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 容易造成存储空间的“碎片”,因为线性表申请内存空间是一整块一整块申请的,那么中间就会造成很多的 ”碎片空间“,而无法使用。
线性表的链式存储结构的特点就是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以在内存中未被占用的任意位置。
现在链式存储结构中,除了要存储数据信息外,还要存储它的后继元素的存储地址(指针)。
也就是说除了存储其本身的信息外,还需要存储一个指示其直接后继的存储位置的信息。
把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素称为存储映像,称为结点(Node)
对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中的第一个结点的存储位置叫做头指针,最后一个结点指针为空(null)
头指针与头节点的异同:
- 头节点的数据域一般不存储任何信息,谁叫它是第一个呢,有这个特权。
- 头指针 是指向链表第一个结点的指针,若链表有头节点,则是指向头结点的指针。
- 头指针具有标识作用,所以常用的头指针冠以链表的名字(指针变量的名字)。
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素。
头结点:
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)。
- 有了头结点,对在第一个元素结点前插入结点和删除第一结点起操作与其他节点的操作就统一了。
- 头结点不一定是链表的必须要素。
单链表图例:
空链表图例:
typedef struct Node //定义结点的数据结构
{
ElemType data; //数据域
struct Node* Next; //指针域
} Node;
typedef struct Node* LinkList; //定义一个结构体指针
单链表常见操作
单链表的读取
- 声明一个结点p指向链表第一个结点,初始化j从1开始。
- 当 j < i 时, 就遍历链表,让 p 的指针向后移动,不断指向下一个结点,j + 1;
- 若到链表末尾 p 为空,则说明第 i 个元素不存在;否则查找成功,返回结点 p 的数据
Status GetElem(LinkList L, int i, ElemType *e)
{
int j;
LinkList p; //声明一个结构指针p
p = L->next; //p指向L第一个结点
j = 1;
while( p && j < i)
{
p = p->next;
j++;
}
if( !p)
{
return ERROR;
}
*e = p->data;
return OK;
}
单链表的读取,说白了,就是从头结点开始找,直到第 i 个元素为止。
由于这个算法的时间复杂度取决于 i 的位置,当 i = 1 时, 则不需要遍历,而 i = n 时则遍历n-1次才可以。因此最坏情况的时间复杂度为O(n).
由于单链表的结构中没有定义表长,所以不能实现知道循环多少次,因此也就不方便使用for来控制循环,而选择while语句。
其核心思想叫做 “工作指针后移”,这是很多算法的常用技术。
单链表的插入
假设存储元素 e 的结点为 s,要将结点 s 插入结点p、p->next 之间的逻辑图示:
一定要注意单链表插入元素时指针的修改顺序!
单链表第 i 个数据插入结点的算法思路:
- 声明一个结点p指向链表头结点,初始化 j 从 1 开始
- 当 j < 1 时,就遍历链表,让 p 的指针向后移动,不断指向下一个结点, j 累加 1;
- 若到链表末尾 p 为空,则说明第 i 个元素不存在;
- 否则查找成功,在系统中生成一个空结点 s;
- 将数据元素e 赋值给s->data
- 执行插入语句,并返回成功
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)); /*生成一个新的结点*/
s->data = e; /*将e赋值给结点s的数据域*/
s->next = p->next; /*插入步骤1*/
p->next = s; /*插入步骤2*/
return OK;
}
单链表的删除
要实现第二个结点的删除操作,其实就是将它的前继结点的指针绕过第二个节点,直接指向第二个结点的后继结点即可。
Status ListDelete(LinkList *L, int i, ElemType *e){
int j;
LinkList p,q;
p = *L;
j = 1;
while( p->next && j<i) //遍历查找第i个元素
{
p = p->next;
++j;
}
if( !(p->next) || j>i )
{
return ERROR;
}
q = p->next;
p->next = q->next; /*这两句相当于p->next = p->next->next;*/
*e = q->data;
free(q);
return OK;
}
单链表的创建
单链表的创建
- 对于顺序存储结构的线性表的整表创建,我们可以用数组的初始化来直观理解。
- 而单链表和顺序存储结构就不一样了,它不像顺序存储结构数据这么集中,它的数据可以分散在内存的各个角落,它的增长也是动态的。
- 对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
创建单链表的过程是一个动态生成链表的过程,从“空表” 的初始状态起,依次建立各元素结点并逐个插入链表。单链表整表创建的算法思路:
- 声明一个结点p和计数器变量 i
- 初始化一个空表L;
- 让 L 的头结点的指针指向NULL, 即建立一个带头结点的单链表;
- 循环实现后继结点的赋值和插入。
头插法建立单链表
- 头插法从一个空表开始,生成新结点,读取数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,知道结束为止。
- 简单来说,就是把新加进来的元素放到表头后的第一个位置:
- 先让新结点的next指针指向头节点之后,然后让表头的next指向新结点
- 就相当于插队一样,只是头插法让新来的结点插到第一个位置。
void CreateListHead(LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); //初始化随机数种子
*L = (LinkList)malloc(sizeof(Node)); //malloc申请一个结点
(*L)->next = NULL; //L为二级指针,头结点
for( i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); //生成新结点
p->data = rand()%100+1; //生成1到100之间的随机数
p->next = (*L)->next;
(*L)->next = p;
}
}
尾插法建立单链表
头插法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。
编程中把新结点直接插入到最后的方式建立单链表就称为尾插法。
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 = (Node *)malloc(sizeof(Node));
p->data = rand()%100+1;
r->next = p;
r = p;
}
}
单链表的整表删除
当我们不打算使用单链表时,需要将其销毁。其实就是在内存中将它释放掉,以便留出空间给其他程序或软件使用。
单链表整表删除算法思路:
- 声明结点p和q;
- 将第一个结点赋值给p,下一个结点赋值给q;
- 循环执行失望p和q赋值给p的操作;
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;
}
注意观察上面代码中while循环体内,可能觉着q变量没有存在的必要,只需要在循环体内直接写free§;p = p->next; 但这样做是不行滴!要知道p是一个结点,它除了数据域,还有指针域。当我们做free§;的时候,其实是对它整个结点进行删除和内存释放的工作,也就是把p的指针域(指向下一个结点)也给删除了,所以需要变量q来保存p的下一个结点。
单链表结构与顺序存储结构优缺点分析
我们分别从存储分配方式、时间性能、空间性能三方面来做对比。
存储分配方式:
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素。
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素。
时间性能:
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
- 顺序存储结构O(1)
- 单链表O(n)
- 查找
- 插入和删除
空间性能:
- 顺序存储结构需要预分配存储空间,分大了,容易造成空间浪费,分小了,容易发生溢出。
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题
顺序表与链表操作总结:
静态链表
用数组描述的链表叫做静态链表,这种描述方法叫做游标实现法。
- 下标为0,数据不存放任何东西,下标为 MAXSIZE-1 时,即999,不存放数据
- 最后一个元素,也就是下标为999,游标 1 指向数组当中第一个数据不为空的元素的下标 1
- 下标 0 所对应的游标 5 指向数组当中没有存放数据的第一个元素,即下标为5的元素
- 其他元素的游标都是直接指向它的下一个元素的下标
线性表的静态链表存储结构
#define MAXSIZE 1000
typedef struct
{
ElemType data; //数据
int cur; //游标(Cursor)
} Component, StaticLinkList[MAXSIZE];
对静态链表进行初始化相当于初始化数组
Status InitList(StaticLinkList space)
{
int i;
for( i=0; i < MAXSIZE-1; i++){
space[i].cur = i + 1; //第i个元素的游标指向i+1
}
space[MAXSIZE-1].cur = 0; //最后一个元素的游标指向0,因为此时为空表
return 0;
}
备忘录:
我们对数组的第一个和最后一个元素做特殊处理,他们的data不存放数据
我们通常把未使用的数组元素称为备用链表
数组的第一个元素,即下标为0的那个元素的cur就存放备用链表的第一个结点的下标(也就是没有存放数据的元素下标)
数组的最后一个元素,即下标为MAXSIZE-1的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用
数组元素的顺序是通过游标和下标进行连接的
静态链表的插入操作
- 静态链表中要解决的是:如何用静态模拟动态链表的存储空间分配,也就是需要的时候申请,不需要的时候释放。
- 在动态链表中,结点的申请和释放分别借用C语言的
malloc()
和free()
两个函数来实现。 - 在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放的问题,所以我们需要自己实现这两个函数,才可以做到插入和删除操作。
- 为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的用游标链成一个备用链表。
- 每当进行插入的时候,便可以从备用链表上取得第一个结点作为待插入的新结点。下面以在A后边插入B为例进行说明。
首先是获得空闲分量的下标:
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur; // i = 5
if( space[0].cur )
{
space[0].cur = space[i].cur; //sapce[0].cur=space[5].cur=6;
}
return i;
}
插入操作的实现代码:
Status ListInsert( StaticLinkList L, int i, ElemType e ){
int j, k, l;
k = MAXSZIE - 1; //数组的最后一个元素的下标,k = 999
if ( i<1 || i > ListLength(L) + 1)
{
return ERROR;
}
j = Malloc_SLL(L); //获得备用链表第一个元素的下标,j = 5;
if( j )
{
L[j].data = e; //L[5].data = B
//i=2,也就是往第二个元素之前插入B
for( l=1; l <= i-1; l++)
{
//L[k].cur=L[MAXSZIE-1].cur,表示第一个有数值的元素的下标
k = L[k].cur; // k = 1;
}
//将插入元素的前一个元素的游标赋值给插入元素的游标
L[j].cur = L[k].cur; //L[5].cur = L[1].cur = 2; B的游标变为2
//将当前插入元素的下标赋值给它的前一个元素的游标
L[k].cur = j; //L[1].cur = 5; A的游标变为5
}
}
静态链表的删除操作
将C删除后,游标2变成游标3,指向下标为3的元素D,同时空出来的元素游标还要归回备用链表中,并且第一个数据的游标需要改变(指向第一个空数据的下标)
Status ListDelte(StaticLinkList L, int i) //i=3,元素为C
{
int j, k;
if( i<1 || i>ListLength(L))
{
return EROOR;
}
k = MAXSZIE-1;
for( j=1; j <= i-1; j++){
k = L[k].cur; //k1 = 1,k2 = 5
}
j = L[k].cur; //j = L[5].cur = 2;
L[k].cur = L[j].cur; //B的游标变为了3
Free_SLL(L, j);
return OK;
}
void Free_SLL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; //把备用链表的第一个元素的下标给了下标k的游标
space[0].cur = k; //静态链表的第一个元素的游标指向k
}
int ListLength(StaticLinkList L)
{
int j = 0;
int i = L[MAXSIZE-1].cur;
while(i)
{
i = L[i].cur;
j++;
}
return j;
}
静态链表的优缺点总结
优点:
- 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。
缺点:
- 没有解决连续存储分配(数组)带来的表长难以确定的问题。
- 失去了顺序存储结构随机存取的特性。
总的来说,静态链表其实是为了给没有指针的编程语言设计的一种实现单链表功能的方法。尽管我们可以用单链表就不用静态链表了,但这样的思考方式是非常巧妙的,应该理解其思想,以备不时之需。
单链表面试题
题目:
快速找到未知长度单链表的中间结点。
- 普通方法很简单,首先遍历一遍单链表以确定单链表的长度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; //定义两个快慢指针,search是快指针,mid是慢指针
mid = search = L; //开始时两个指针都指向链表的头结点
while(search->next != NULL)
{
//search移动的速度时mid的两倍
if(search->next->next != NULL)
{
search = search->next->next;
}
else
{
search = search->next;
}
mid = mid->next;
}
*e = mid->data; //获取中间结点的值
return Ok;
}
循环链表
对于单链表,由于每个结点只存储了向后的指针,到了尾部标识就停止了向后链的操作。也就是说,按照这样的方式,只能索引后继结点不能索引前驱结点。这样一来,不从头结点出发,这样就无法访问到全部结点。
为了解决这个问题,我们只需要将单链表的尾结点的指针由空指针改为指向头结点的指针,问题就结了。
将单链表中尾结点的指针由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表成为单循环链表,简称循环链表
注:这里并不是说循环链表一定有头结点
其实循环链表的单链表的主要差异就在于循环的判断空链表的条件上,原来判断head->next是否为空,现在则是head->next是否等于head;
其实循环链表的单链表的主要差异就在于循环的判断空链表的条件上,原来判断head->next是否为空,现在则是head->next是否等于head;
终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点是rear->next->next,当然也是O(1)
循环链表的初始化操作
typedef struct CLinkList
{
int data;
struct CLinkList *next;
}node;
/*初始化链表*/
void ds_init(node **pNode) //输入参数为一个结点指针
{
int item;
node *temp;
node *target; //声明结点
printf("请输入结点的值,输入0完成初始化\n");
while(1)
{
scanf("%d",&item);
fflush(stdin);
if(item == 0)
return;
if((*pNode) == NULL) //链表只有一个结点
{
*pNode = (node*)malloc(sizeof(struct CLinkList));
if(!(*pNode))
exit(0);
(*pNode)->data = item;
(*pNode)->next = *pNode;
}
else
{
//找到next指向第一个结点的结点
for(target = (*pNode); target->next != (*pNode); target = target->next)
;
temp = (node *)malloc(sizeof(struct CLinkList));
if(!temp)
exit(0);
temp->data = item;
temp->next = *pNode;
target->next = temp;
}
}
}
循环链表的插入操作
/*插入结点*/
void ds_insert(node **pNode, int i)
{
node *temp;
node *target;
node *p;
int item;
int j = 1;
printf("请输入要插入结点的值:");
scanf("%d", &item);
if(i == 1)
{
//新插入的结点作为第一个结点
temp = (node *)malloc(sizeof(struct CLinkList)); //分配空间
if(!temp)
exit(0);
temp->data = item;
/*寻找到最后一个结点*/
for(target = (*pNode); target->next != (*pNode); target = target->next)
;
temp->next = (*pNode); //将开始的头结点给予temp->next
target->next = temp; //令temp等于target->next
*pNode = temp; //temp成为新的头结点
}
else
{
target = *pNode; //target是目标指针,pNode是指向第一个结点的指针
for(; j < (i-1); ++j)
{
target = target->next;
} //输入i = 2的话,循环结束时target指向的是第二个元素
temp = (node *)malloc(sizeof(struct CLinkList)); //开辟内存空间
if(!temp)
exit(0);
temp->data = item; //要插入的数据的值
p = target->next; //令目标指针暂存在p中
target->next = temp;
temp->next = p;
}
}
循环链表的删除操作
void ds_delete(node **pNode, int i)、
{
node *target;
node *temp;
int j = 1;
if(i == 1)
{
for(target = *pNode; target->next != *pNode; target = target->next)
;
temp = *pNode;
*pNode = (*pNode)->next;
target->next = *pNode;
free(temp); //free掉原来的头结点
}
else
{
target = *pNode;
for(; j < i-1; ++j)
{
target = target->next; //使用target指针寻找目标
}
temp = target->next;
target->next = temp->next;
free(temp);
}
}
循环链表的查找操作
/*返回结点所在位置*/
int ds_search(node *pNode, int elem)
{
node *target;
int i = 1;
for(target = pNode; target->data != elem && target->next != pNode; ++i)
{
target = target->next;
}
if(target->next == pNode) /*表中不存在该元素*/
{
if(target->data == elem){
return i;
}
return 0;
}
else
return i;
}
约瑟夫环问题
//定义链表节点
typedef struct node
{
int data;
struct node *next;
}node;
node *create(int n) //创建一个链表
{
node *p = NULL, *head;
head = (node*)malloc(sizeof(node)); //创建一个头结点
p = head; //指向当前结点的指针,p是经常变的
node *s; //s也是做临时使用
int i = 1;
if( 0 != n)
{
while(i <= n)
{
s = (node*)malloc(sizeof(node)); //第一个节点
s->data = i++; //为循环链表初始化,第一个结点为i,第二个结点为2
p->next = s; //头结点的next即第一个结点等于s
p = s; //再令头结点等于s ,循环往复
}
s->next = head->next; //构成循环链表
}
free(head); //去掉头结点
return s->next; //返回头结点,最后一个结点的next就是头结点
}
int main()
{
int n = 41; //有41个人
int m = 3;
int i;
node *p = create(n);
node *temp; //临时指针
while (p != p->next) //不是空表执行while循环
{
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;
}
循环链表的特点
- 在单链表中,我们有了头结点时,我们可以用O(1)的时间访问第一个结点,但对于要访问的最后一个结点,我们必须挨个向下索引,所以需要O(n)
- 对于循环链表,用O(1)的时间就可以有链表指针访问到最后一个结点。
- 我们定义一个用于指向终端结点的尾指针来表示循环链表,此时查找开始结点和终端结点就很方便了。
题目:实现将两个线性表连接成一个线性表的运算
分析:
- 若在单链表或头指针表示的单链表上做这种链接操作,都需要遍历第一个链表,找到最后一个结点,然后将第二个链表链接到第一个的后面,其执行时间是O(n)
- 若在尾指针表示的单循环链表上实现,则只需要修改指针,无需遍历,其执行时间是O(1)
LinkList Connect(LinkList A, LinkList B) //假设A,B为非空循环链表的尾指针
{
LinkList p = A->next; //保存A表的头结点位置
A->next = B->next->next; //B表的开始结点链接到A表尾
free(B->next); //释放B表的头结点
B->next = p;
return B; //返回新循环链表的尾指针
}
经典题目解析
题目一: 判断单链表中是否有环
有环的定义是,链表的尾结点指向了链表中的某个结点。
解析:
方法一:使用p,q 两个指针,p 总是向前走,但 q 每次都从头开始走,对于每个结点,看p走的步数是否和 q 一样。如下面的图,当 p 从1走到3时用了5步,此时若q从head出发,则需要两步就到了3,因而步数不等,出现矛盾,存在环。
int HasLoop_StepCount(LinkList L)
{
LinkList p = L; // 定义结点 p
int step_p = 0; // 指针 p 走过的步数
while(p)
{ // p 结点存在
LinkList q = L; // 定义结点 q
int step_q = 0; // 指针 q 的步数
while(q)
{ // q 指向的结点不为空
if(q == p)
{ // 当p与q到达相同结点时
if(step_p == step_q) // 走过的步数一样
break; // 说明没有环
else // 否则
{
printf("环的位置在第%d个结点处。\n\n", step_q);
return 1; // 有环并返回1
}
}
q = q->next; // 如果没发现环,继续下一个结点
step_q++; // step_q 步数自增
}
p = p->next; // p继续向后一个结点
step_p++; // step_p 步数自增
}
return 0;
}
方法二:使用p,q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p == q,则存在环。(快慢指针法)
int hasLoop_SpeedPointer(LinkList L)
{
int step1 = 1;
int step2 = 2;
LinkList p = L;
LinkList q = L;
while(p != NULL && q != NULL && q->next != NULL)
{
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;
}
题目二:
魔术师发牌问题
题目描述:魔术师利用一副牌中的13张黑牌,预先将他们排好后叠放在一起,牌面朝下。对观众说:“我不看牌,只数数就可以猜到每张牌是什么,我大声数数,你们听,不信?现场演示。”魔术师将最上面的那张牌数为1,把他翻过来正好是黑桃A,将黑桃A放在桌子上,第二次数1,2,将第一张牌放在这些牌的下面,将第二张牌翻过来,正好是黑桃2,也将它放在桌子上这样依次进行将13张牌全部翻出,准确无误。
问题:牌的开始顺序是如何安排的?
void Magician(LinkList head)
{
LinkList p;
int j;
int Countnumber = 2;
p = head;
p->data = 1; //第一张牌放1
while(1)
{
for( j = 0; j < Countnumber; j++) //需要执行Counternmber次,
{ //相当于向前走了Countnumber
p = p->next; //p指向下一个位置
if(p->data != 0) //初始化时牌全为0
{
p = p->next;
j--; //j--就相当于跳过已经有值的存储单元,让p再往前走一步
}
}
if(p->data == 0)
{
p->data == CountNumber; //填充数据
CountNumber++;
if(CountNumber == 14)
break; //跳出循环
}
}
}
题目三:
拉丁方阵问题
问题描述:拉丁方阵是一种n×n的方阵,方阵中恰有n种不同的元素,每种元素恰有n个,并且每种元素在一行和一列中 恰好出现一次。著名数学家和物理学家欧拉使用拉丁字母来作为拉丁方阵里元素的符号,拉丁方阵因此而得名。
拉丁方阵小故事:拉丁方阵追溯到18世纪的欧洲,据说普鲁士的腓特列大帝曾组成一支仪仗队。仪仗队共有36名军官,来自6支部队,每支部队中,上校、中校、少校、上尉、中尉、少尉各一名。他希望这36名军官排成6×6的方阵,方阵的每一行,每一列的6名军官来自不同的部队并且军衔各不相同。后来,他去求教瑞士著名的大数学家欧拉。欧拉发现这是一个不可能完成的任务。
void Latin_square(LinkList head)
{
LinkList p;
int n; //方阵的大小
int i,j; //计数器
p = head;
p_cur = head;
for(i = 0; i < n; i++){
for(j = 0; i < n; j++)
{
printf("%d,", p_cur->data);
p_cur = p_cur->next;
}
printf("\n");
p = p->next;
p_cur = p;
}
}
链式存储结构之双向链表与跳表
只要大家坐过火车,对于双向链表的理解就相当简单。双向链表就是在单链表的基础之上,为每一个结点增加了它的前继结点。双向链表的定义如下:
typedef struct DaulNode
{
ElemType data;
struct DaulNode *prior; //前驱结点
struct DaulNode *next; //后继结点
}DaulNode, *DuLinkList;
单链表存在循环链表,双向链表也有循环链表:
双向链表的插入操作
插入操作其实并不复杂,不过顺序很重要,千万不要写反了。
双向链表的删除操作
程序员永远都知道,删除一个元素更加容易,你说呢?
双向链表相对于单链表来说,是更复杂一些,每个结点多了一个prior指针,对于插入和删除操作的顺序一定要格外小心,就像你的 “求生欲”。
双向链表可以有效提高算法的时间性能,说白了就是用空间来换取时间。
双向链表的两个实例
凯撒密码
题目描述:
- –要求实现用户输入一个数使得26个字母的排列发生变化,例如用户输入3,输出结果:
- –DEFGHIJKLMNOPQRSTUVWXYZABC
- –同时需要支持负数,例如用户输入-3,输出结果:
- –XYZABCDEFGHIJKLMNOPQRSTUVW
双向循环链表初始化操作(InitList()函数)执行流程:
Caesar()函数的执行过程: