四、线性表(六)、 循环链表 约瑟夫问题

数据结构目录

1.概念

对于单链表,由于每个结点值存储了向后的指针,到了尾部标识就停止了向后链的操作。也就是说,按照这样的方式,只能索引后继结点不能索引前驱结点。

这样的话,假如不从头结点触发,就无法访问到全部结点。为了解决这个问题,我们将单链表中终端结点的指针由空指针改为指向头结点,问题就解决了。

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

循环链表

注意:
并不是说循环链表一定要有头结点
循环链表和单链表的主要差异就在于循环的判断空链表的条件上,单 链表是判断head->next是否为NULL,现在则是head->next是否等于head

下方示例都以尾指针rear来指代循环链表而不是头指针,而且示例中的循环链表都没有头结点,也就是说,循环链中第一个结点,实际上是尾结点(这点需要发挥一点想象力)
由于终端结点用尾指针rear指示,则查找终端结点的复杂度为O(1),而开始结点是rear->next->next,当然也是O(1)

2. 循环链表的定义

typedef struct Node{
    ElemType data;
    struct Node *next;
}Node;
typedef struct Node *CircleLinkList;

3. 循环链表的创建

void initCircleLinkList(CircleLinkList *L){
    //表示输入的内容
    int item;
    CircleLinkList temp;
    //target每次用来标记尾结点
    CircleLinkList target = NULL;
    printf("输入结点的值,输入0表示完成初始化\n");
    
    while (1) {
        scanf("%d",&item);
        fflush(stdin);
        
        if (item == 0) {
            return;
        }
        if (*L == NULL) {
            /*循环链表为空 则创建第一个结点 作为尾结点*/
            *L = (CircleLinkList)malloc(sizeof(Node));
            if (!(*L)) {
                exit(0);
            }
            (*L)->data = item;
            (*L)->next = *L;
        } else {
            //找到尾结点前面第一个结点
            for (target = *L; target->next != *L; target = target->next);
            temp = (CircleLinkList)malloc(sizeof(Node));
            if (!temp) {
                exit(0);
            }
            //在尾结点之前插入一个新的结点
            temp->data = item;
            temp->next = *L;
            target->next = temp;
        }
    }
}

4. 循环链表的插入

/// 插入结点
/// @param L 链表的尾结点
/// @param i 插入的位置
void insertCircleLinkList(CircleLinkList *L,int i){
    CircleLinkList temp,target;
    int item,j = 1;
    printf("请输入要插入结点的值:");
    scanf("%d",&item);
    
    if (i == 1) {
        //i==1 表示要替换到之前的尾结点
        temp = (CircleLinkList)malloc(sizeof(Node));
        if (!temp) {
            exit(0);
        }
        temp->data = item;
        //循环得到尾结点前面一个结点
        for (target = *L; target->next != *L; target = target->next);
        
        temp->next = *L;
        target->next = temp;
        //将新结点放在尾结点位置
        *L = temp;
    } else {
        target = *L;
        //通过i-2次遍历,得到i位置前面一个结点
        for (; j < (i-1); ++j) {
            target = target->next;
        }
        temp = (CircleLinkList)malloc(sizeof(Node));
        if (!temp) {
            exit(0);
        }
        temp->data = item;
        temp->next = target->next;
        target->next = temp;
    }
}

5.循环链表的删除

void deleteCircleLinkList(CircleLinkList *L,int i){
    CircleLinkList target,temp;

    if (i == 1) {
        //删除尾指针的结点
        //获取到尾结点前面的一个结点
        for (target = *L; target->next != *L; target = target->next);
        //重新指向
        temp = *L;
        *L = (*L)->next;
        target->next = *L;
        //释放结点
        free(temp);
    } else {
        //获取到i-1位置的结点
        target = *L;
        for (int j = 1; j < i-1; j++) {
            target = target->next;
        }
        temp = target->next;
        target->next = target->next->next;
        free(temp); 
    }
}

6.循环链表返回结点的位置

//通过数据的值,得到结点位置
int searchCircleLinkList(CircleLinkList L,int elem){
    CircleLinkList target;
    int i = 1;
    for (target = L; target->data != elem && target->next != L; ++i) {
        target = target->next;
    }
    if (target->next == L) {
        //找了一圈还是没找到
        return 0;
    }
    return i;
}

7、循环链表的特点

循环链表
  • 使用rear是否等于rear->next来判断循环链表是否为空表

一个例子:


合并两个链表

题目:实现将两个线性表(a1,a2,...,an)和(b1,b2,...,bm)连接成一个线性表(a1,...an,b1,...bm)的运算
分析:
若在单链表或头指针表示的单循环表上左这种链接操作,都需要遍历第一个链表,找到结点an,然后将结点b1链到an的后面,其执行时间是O(n)
若在尾指针表示的单循环链表上实现,则只需要修改指针,无需遍历,其执行时间为O(1)

CircleLinkList connectLinkList(CircleLinkList L1,CircleLinkList L2){
    CircleLinkList head = L1->next; //保存L1的头结点位置
    L1->next = L2->next->next; //L1的尾结点指向L2的第一个结点
    free(L2->next); //是否L2的头结点
    L2->next = head; //让L2的尾结点指向L1的头结点
    
    return L2;
}

8、约瑟夫问题

据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

解题思路:

  • 创建一个包含41个结点的循环链表(没有头结点),每个节点的数据域都存放一个序号,分别为1-41
  • 模拟计数过程,每次数到3则删除这个节点,并在下一个节点开始重新计数,最后得到免死的序号与人数
//创建一个不包含头结点的循环链表
Node *create(int n){
    //head用于存储第一个结点的地址
    Node *p = NULL,*s = NULL,*head = NULL;
    
    for (int i = 1;i <= n; i++) {
        p = (Node *)malloc(sizeof(Node));
        p->data = i;
        if (i == 1) {
            //第一个结点
            head = s = p;
        } else {
            s->next = p;
            s = p;
        }
    }
    //构成循环
    s->next = head;
    
    return head;
}
//解决约瑟夫问题,返回免死的人数
void soulutionProblem(int *args){
    //n表示总人数,m表示数到几
    int n = 41,m = 3;
//    scanf("%d%d",&n,&m);
    Node *L = create(n);
    Node *p = L;
    
    int counter = 1;
    while (p != p->next) {
        if (counter % m == 0) {
            //数到了这个数字
            /*
             这个就是需要自杀的人 我们删除这个节点
             然后再从下一个开始再排
             */
            Node *temp = p->next;
            p->next = temp->next;
            free(temp);
            p = p->next;
            counter = 1;
            n--;
            if (n < m) {
                //如果剩下总人数比计数的总人数还少的时候
                //剩下的这些人就可以耍赖不自杀了
                *args = n;
                while (p != p->next) {
                    printf("%d\n",p->next->data);
                    Node *temp = p->next;
                    p->next = temp->next;
                    free(temp);
                    p = p->next;
                }
                printf("%d\n",p->data);
                free(p);
                
                break;
            }
        } else {
            counter++;
            if (counter % m != 0) {
                p = p->next;
            }
        }
    }
}

9.约瑟夫问题简易方法

推导过程可以查看约瑟夫简便方法 或者 约瑟夫问题百度百科

大致思路就是逆推得到n人、m个数的问题,可以由(n-1)人推导到位置,依次类推,最后得到单次的公式为(ans + m)% n,其中ams为最后出列人的序号,m表示要数的数字,n表示单次总人数,下面给出代码

void simpleSolution(){
    int n, m, i, s = 0;
    n = 41;m = 3;
    scanf("%d%d", &n, &m);
    //经过n-1次的推导,单次的总人数为i
    for (i = 2; i <= n; i++)
    {
        s = (s + m) % i;
        printf("%d\n",s);
    }
    printf ("\nThe winner is %d\n", s+1);
}

这种方法的缺点是只能得到最终一个人,不能求得其它可以免于自杀的人的序号

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343