7-无头双向非循环链表

目录

1.定义

2.代码

2.1.车厢类

2.2.火车类

2.2.1.在链表的头部添加节点

2.2.2.在链表的尾部添加节点

2.2.3.在链表的任意位置index处添加节点

2.2.4.查询链表的任意位置index处的节点值

2.2.5.修改链表中任意位置index处的节点值

2.2.6.传入一个双向链表节点,将该节点从双向链表中删除

2.2.7.删除链表中第一次出现某个值的节点

2.2.8.删除链表中值为val的所有节点

2.2.9.判断链表的index是否合法

2.2.10.根据index与size的大小关系快速定位指定index位置的节点

2.2.11.toString()方法

2.3.总代码实现


1.定义

既要存储下一个节点地址,还要存储前一个节点地址。是Java的集合框架中LinkedList底层实现。

2.代码

2.1.车厢类

/**
 * 车厢类
 */
class DoubleNode{
    //指向前驱节点
    DoubleNode prev;
    //存储的具体元素
    int val;
    //指向后继节点
    DoubleNode next;

    //有参构造
    public DoubleNode(int val){
        this.val=val;
    }
}

2.2.火车类

/**
 * 火车类 基于int的双向链表
 */
public class DoubleLinkedList {
    //存储的具体车厢个数
    private int size;
    //头节点
    private DoubleNode first;
    //尾节点
    private DoubleNode last;
 
    //具体方法实现
    //...
}

2.2.1.在链表的头部添加节点

/**
 * 头插
 * @param val
 */
public void addFirst(int val) {
    //引入一个局部变量,暂时存储一下头节点的地址
    DoubleNode f = first;
    //要插入一个节点,首先要new一个节点
    DoubleNode node = new DoubleNode(val);
    first = node; //因为是头插,那么这行代码不管当前链表是否为空都会执行
    if(f == null) { //若链表为空,则新建的节点既是头节点又是尾节点
        last = node;
    } else {
        node.next = f;
        f.prev = node;
    }
    size++;
}

2.2.2.在链表的尾部添加节点

/**
 * 尾插
 * @param val
 */
public void addLast(int val) {
    //引入一个局部变量,暂时存储一下尾节点的地址
    DoubleNode l = last;
    DoubleNode node = new DoubleNode(val);
    last = node;
    if(l == null) {
        first = node;
    } else {
        node.prev = l;
        l.next = node;
    }
    size++;
}

2.2.3.在链表的任意位置index处添加节点

/**
 * 任意index处添加元素
 * @param val
 */
public void addIndex(int index, int val) {
    if(index < 0 || index > size) {
        System.err.println("add index illegal!");
        return;
    } else if(index == 0) {
        addFirst(val);
    } else if(index == size) {
        addLast(val);
    } else {
        //此时0 < index < size
        //在报错方法(未写)上使用快捷键Alt+Enter,自动生成指定方法
        DoubleNode node = node(index);
        //指向当前位置的前驱节点
        DoubleNode prev = node.prev;
        //要插入的新节点
        DoubleNode newNode = new DoubleNode(val);
        //先处理后半部分引用链
        newNode.next = node; ①
        node.prev = newNode; ②
        //再处理前半部分引用链
        prev.next = newNode; ③
        newNode.prev = prev; ④
        size++;
    }
}

2.2.4.查询链表的任意位置index处的节点值

/**
 * 根据index索引取得节点值
 * @param index
 * @return
 */
public int get(int index) {
    if(rangeCheck(index)) {
        DoubleNode node = node(index);
        return node.val;
    } else {
        System.out.println("get index illegal!");
        return -1;
    }
}

2.2.5.修改链表中任意位置index处的节点值

/**
 * 修改index处的值为newVal
 * @param index
 * @param newVal
 */
public void set(int index, int newVal) {
    if(rangeCheck(index)) {
        DoubleNode node = node(index);
        node.val = newVal;
    } else {
        System.out.println("set index illegal!");
    }
}

2.2.6.传入一个双向链表节点,将该节点从双向链表中删除

/**
 * 传入一个双向链表节点,将该节点从双向链表中删除
 * 核心思想 分治,先处理前驱或后继,再处理另一半的情况
 * @param node
 */
private void unlinkNode(DoubleNode node) {
    //待删除节点的前驱
    DoubleNode prev = node.prev;
    //待删除结点的后继
    DoubleNode next = node.next;
        
    //先处理前半部分引用链
    //判断边界
    if(prev == null) {
        //说明此时待删除的节点恰好是头节点
        first = next;
    } else {
        //此时前驱节点不为空
        prev.next = next;
        node.prev = null;
    }
        
    //再处理后半部分引用链
    if(next == null) {
        last = prev;
    } else {
        //此时后继节点不为空
        next.prev = prev;
        node.next = null;
    }
    size--;
}

2.2.7.删除链表中第一次出现某个值的节点

/**
 * 删除第一次出现值为val的元素
 * @param val
 */
public void removeValueOnce(int val) {
    //只需要从头开始遍历
    for (DoubleNode x = first; x != null; x = x.next) {
        if(x.val == val) {
            unlinkNode(x);
            return;
        }
    }
}

2.2.8.删除链表中值为val的所有节点

/**
 * 删除单链表中所有值为val的节点
 * @param val
 */
public void removeAllValue(int val) {
    for(DoubleNode x = first; x != null;) {
        if(x.val == val){
            //x就是待删除的元素
            //先暂存一下next的节点地址
            DoubleNode next = x.next;
            unlinkNode(x);
            x = next; //真正让x走向next
        } else {
            x = x.next;
        }
    }
}

2.2.9.判断链表的index是否合法

/**
 * 判断链表的index是否合法
 * @param index
 * @return
 */
private boolean rangeCheck(int index) {
    if(index < 0 || index >= size) {
        return false;
    }
    return true;
}

2.2.10.根据index与size的大小关系快速定位指定index位置的节点

/**
 * 根据index与size的大小关系快速定位指定index位置的节点
 * @param index
 * @return
 */
private DoubleNode node(int index) {
    if(index < size >> 1) {
        //从头向后
        DoubleNode node = first;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        return node;
    } else {
        //从后向前
        DoubleNode node = last;
        for (int i = size-1; i > index; i--) {
            node = node.prev;
        }
        return node;
    }
}

2.2.11.toString()方法

public String toString() {
    String ret = "";
    DoubleNode node = first;
    while(node != null) {
        ret += node.val + "->";
        node = node.next;
    }
    ret += "NULL";
    return ret;
}

2.3.总代码实现

/**
 * 车厢类
 */
class DoubleNode{
    //指向前驱节点
    DoubleNode prev;
    //存储的具体元素
    int val;
    //指向后继节点
    DoubleNode next;

    //有参构造
    public DoubleNode(int val){
        this.val=val;
    }
}

/**
 * 火车类 基于int的双向链表
 */
public class DoubleLinkedList {
    //存储的具体车厢个数
    private int size;
    //头节点
    private DoubleNode first;
    //尾节点
    private DoubleNode last;

    /**
     * 头插
     * @param val
     */
    public void addFirst(int val){
        //引入一个局部变量,暂时存储一下头节点的地址
        DoubleNode f=first;
        //要插入一个节点,首先要new一个节点
        DoubleNode node=new DoubleNode(val);
        first=node;
        if(f==null){
            last=node;
        }else{
            node.next=f;
            f.prev=node;
        }
        size++;
    }

    /**
     * 尾插
     * @param val
     */
    public void addLast(int val) {
        //引入一个局部变量,暂时存储一下尾节点的地址
        DoubleNode l = last;
        DoubleNode node = new DoubleNode(val);
        last = node;
        if(l == null) {
            first = node;
        } else {
            node.prev = l;
            l.next = node;
        }
        size++;
    }

    /**
     * 任意index处添加元素
     * @param val
     */
    public void addIndex(int index, int val) {
        if(index < 0 || index > size) {
            System.err.println("add index illegal!");
            return;
        } else if(index == 0) {
            addFirst(val);
        } else if(index == size) {
            addLast(val);
        } else {
            //此时0 < index < size
            //在报错方法(未写)上使用快捷键Alt+Enter,自动生成指定方法
            DoubleNode node = node(index);
            //指向当前位置的前驱节点
            DoubleNode prev = node.prev;
            //要插入的新节点
            DoubleNode newNode = new DoubleNode(val);
            //先处理后半部分引用链
            newNode.next = node;
            node.prev = newNode;
            //再处理前半部分引用链
            prev.next = newNode;
            newNode.prev = prev;
            size++;
        }
    }

    /**
     * 根据index索引取得节点值
     * @param index
     * @return
     */
    public int get(int index) {
        if(rangeCheck(index)) {
            DoubleNode node = node(index);
            return node.val;
        } else {
            System.out.println("get index illegal!");
            return -1;
        }
    }

    /**
     * 修改index处的值为newVal
     * @param index
     * @param newVal
     */
    public void set(int index, int newVal) {
        if(rangeCheck(index)) {
            DoubleNode node = node(index);
            node.val = newVal;
        } else {
            System.out.println("set index illegal!");
        }
    }

    /**
     * 传入一个双向链表节点,将该节点从双向链表中删除
     * 核心思想 分治,先处理前驱或后继,再处理另一半的情况
     * @param node
     */
    private void unlinkNode(DoubleNode node) {
        //待删除节点的前驱
        DoubleNode prev = node.prev;
        //待删除结点的后继
        DoubleNode next = node.next;

        //先处理前半部分引用链
        //判断边界
        if(prev == null) {
            //说明此时待删除的节点恰好是头节点
            first = next;
        } else {
            //此时前驱节点不为空
            prev.next = next;
            node.prev = null;
        }

        //再处理后半部分引用链
        if(next == null) {
            last = prev;
        } else {
            //此时后继节点不为空
            next.prev = prev;
            node.next = null;
        }
        size--;
    }

    /**
     * 删除第一次出现值为val的元素
     * @param val
     */
    public void removeValueOnce(int val) {
        //只需要从头开始遍历
        for (DoubleNode x = first; x != null; x = x.next) {
            if(x.val == val) {
                unlinkNode(x);
                return;
            }
        }
    }

    /**
     * 删除单链表中所有值为val的节点
     * @param val
     */
    public void removeAllValue(int val){
        for(DoubleNode x=first;x!=null;){
            if(x.val==val){
                //x就是待删除的元素
                //先暂存一下next的节点地址
                DoubleNode next=x.next;
                unlinkNode(x);
                x=next;
            }else{
                x=x.next;
            }
        }
    }

    /**
     * 根据index与size的大小关系快速定位指定index位置的节点
     * @param index
     * @return
     */
    private DoubleNode node(int index) {
        if(index < size >> 1) {
            //从头向后
            DoubleNode node = first;
            for (int i = 0; i < index; i++) {
                node = node.next;
            }
            return node;
        } else {
            //从后向前
            DoubleNode node = last;
            for (int i = size-1; i > index; i--) {
                node = node.prev;
            }
            return node;
        }
    }

    /**
     * 判断链表的index是否合法
     * @param index
     * @return
     */
    private boolean rangeCheck(int index) {
        if(index < 0 || index >= size) {
            return false;
        }
        return true;
    }

    public String toString() {
        String ret = "";
        DoubleNode node = first;
        while(node != null) {
            ret += node.val + "->";
            node = node.next;
        }
        ret += "NULL";
        return ret;
    }
    
    public static void main(String[] args) {
        DoubleLinkedList doubleLinkedList=new DoubleLinkedList();
        doubleLinkedList.addFirst(1);
        doubleLinkedList.addFirst(2);
        doubleLinkedList.addFirst(3);
        doubleLinkedList.addLast(3);
        doubleLinkedList.addLast(3);
        doubleLinkedList.addLast(3);
        //3->2->1->3->3->3->NULL
        doubleLinkedList.addIndex(1,99);
        //3->99->2->1->3->3->3->NULL
        doubleLinkedList.removeValueOnce(99);
        //3->2->1->3->3->3->NULL
        doubleLinkedList.removeAllValue(3);
        //2->1->NULL
        System.out.println(doubleLinkedList);
    }
}

<think>我们正在处理一个关于单向无头循环链表的问题。根据用户需求,我们需要解释其结构和操作。 首先,单向无头循环链表的特点是: 1. 没有头结点(无头):链表的第一个节点就是数据节点。 2. 循环链表的最后一个节点的next指针指向第一个节点,形成一个环。 注意:由于没有头结点,所以操作时需要特别注意空链表的情况,以及循环的特性。 主要操作包括: 1. 初始化 2. 插入(头插、尾插、任意位置插入) 3. 删除(头删、尾删、任意位置删除) 4. 查找 5. 遍历 6. 销毁 由于是循环链表,我们需要特别注意: -链表时,指向链表的指针(比如我们通常用的phead)应该为NULL。 -链表中只有一个节点时,该节点的next指向自己。 下面我们逐步实现这些操作(以C语言为例,但重点在于解释原理)。 定义节点结构: typedef struct Node { int data; struct Node* next; } Node; 我们用一个指向链表的指针(通常叫phead)来指向链表的第一个节点(如果没有节点则为NULL)。 1. 初始化:初始时链表为空,所以phead = NULL。 2. 插入操作: a. 头插:在链表的第一个节点前插入一个新节点,使其成为新的第一个节点。 - 如果链表为空:新节点的next指向自己,然后phead指向新节点。 - 如果链表不为空:新节点的next指向原来的第一个节点,然后需要找到尾节点(尾节点的next指向原来的第一个节点),将尾节点的next指向新节点,最后phead指向新节点(因为新节点成为新的第一个节点)。 - 注意:由于循环链表,尾节点的查找可以通过phead开始遍历直到某个节点的next等于phead。 b. 尾插:在链表的最后一个节点后插入一个新节点。 - 如果链表为空:同头插(实际上头插和尾插在空链表时一样)。 - 不为空:找到尾节点(尾节点的next等于phead),将尾节点的next指向新节点,新节点的next指向phead(即第一个节点)。 3. 删除操作: a. 头删:删除第一个节点。 - 如果链表为空:不能删除,返回错误。 - 如果只有一个节点:删除该节点,并将phead置为NULL。 - 多个节点:先找到尾节点(因为尾节点的next指向第一个节点),将尾节点的next指向第二个节点,然后删除第一个节点,并将phead指向原来的第二个节点(即新的第一个节点)。 b. 尾删:删除最后一个节点。 -链表:返回错误。 - 只有一个节点:同头删(删除后链表为空)。 - 多个节点:找到倒数第二个节点(因为它的next指向尾节点,而尾节点是最后一个节点),将倒数第二个节点的next指向phead(即第一个节点),然后释放尾节点。 4. 查找:从phead开始遍历,直到再次遇到phead(注意循环终止条件)。 5. 遍历:同样从phead开始,依次访问每个节点,直到回到phead(注意空链表情况)。 6. 销毁:需要逐个释放节点,注意循环链表不能直接循环遍历(因为最后回到起点,无法判断结束),所以我们可以先断开循环,变成单向链表再释放,或者使用一个指针记录起始点,当遍历一圈回到起始点时停止。但更简单的方法是:先保存第一个节点,然后从第二个节点开始删除,直到下一个节点是第一个节点时停止,最后删除第一个节点。或者,我们可以先找到尾节点,将尾节点的next置为NULL,然后按照非循环链表的方式释放,但这样破坏了结构。另一种方法是使用两个指针,一个当前指针,一个下一个指针,每次删除当前指针,然后移动到下一个,直到下一个指针是起始点。 注意:由于循环链表,在操作时一定要维护好循环结构,特别是在插入和删除时。 下面我们给出一些关键操作的代码示例(注意:这里我们假设链表至少有一个节点,但实际代码中需要处理空链表情况)。 为了更清晰,我们分别实现: 头插法: void InsertFront(Node** phead, int data) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; if (*phead == NULL) { // 空链表 newNode->next = newNode; // 指向自己 *phead = newNode; } else { // 找到尾节点:从*phead开始遍历直到next等于*phead Node* tail = *phead; while (tail->next != *phead) { tail = tail->next; } // 现在tail是尾节点 newNode->next = *phead; // 新节点指向原头 tail->next = newNode; // 尾节点指向新节点 *phead = newNode; // 更新头指针 } } 但是,上面的头插需要遍历整个链表(找尾节点),效率不高。我们可以优化:实际上,在循环链表中,我们可以不遍历而直接插入,但需要调整指针,同时更新头指针,并且注意尾节点指向新头。然而,我们也可以不找尾节点,而是采用另一种方法:先插入到第一个节点之后,然后交换两个节点的数据。这样就不需要遍历了。 优化头插: 步骤: 1. 创建新节点。 2. 如果链表为空,则处理同上。 3. 否则,将新节点插入到第一个节点之后(即新节点的next指向第一个节点的next,第一个节点的next指向新节点)。 4. 交换第一个节点和新节点的数据。 5. 这样,新节点就变成了第一个节点,而原来的第一个节点变成了第二个节点。 代码: void InsertFront(Node** phead, int data) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; if (*phead == NULL) { newNode->next = newNode; *phead = newNode; } else { // 插入到第一个节点后面 newNode->next = (*phead)->next; (*phead)->next = newNode; // 交换数据 int temp = (*phead)->data; (*phead)->data = newNode->data; newNode->data = temp; // 现在新节点在第一个节点后面,但数据被交换,所以第一个节点的数据变成了新数据 // 注意:此时*phead指向的节点数据是新的data,而新节点的数据是原来的第一个节点的数据 // 这样,我们就相当于在头部插入了一个节点,而且不需要更新*phead(因为第一个节点还是原来那个节点,只是数据被交换了) // 但是,这样操作后,实际上新节点在第二个位置,但数据在第一个节点上,而第一个节点原来的数据在第二个节点上。 // 所以,从逻辑上,第一个节点变成了新节点吗?并没有,第一个节点还是原来那个节点,只是数据被交换了。 // 所以,这种方法实际上并没有在头部插入一个新节点,而是将新节点插入在第二个位置,然后交换了数据。 // 因此,我们不需要更新*phead,因为第一个节点还是原来的节点(地址没变),但它的数据变成了新数据。 // 所以,这样也实现了在头部插入数据的效果。 } } 注意:这种方法虽然避免了遍历,但是改变了节点的地址关系。如果我们希望新节点成为第一个节点(地址改变),那么这种方法就不合适了。因为头指针仍然指向原来的第一个节点(只是数据被交换了)。所以,如果外部代码依赖于节点的地址,这种方法就不行。 因此,我们回到第一种方法(遍历找尾)或者另一种方法:在插入后,不交换数据,而是更新头指针,并且将新节点作为头,同时将尾节点指向新头。但这样需要遍历。 或者,我们可以维护一个尾指针tail,这样头插和尾插都很快。因为循环链表中,如果知道尾指针,那么头指针就是tail->next。所以,我们可以用一个指向尾节点的指针来代表整个链表。这样: 头插:新节点插入在尾节点后面(即头节点前面),然后更新头节点(即尾节点的next)为新节点(但头节点是尾节点的next,所以插入后头节点就变成了新节点),同时尾节点不变(因为插入在尾节点后面,所以新节点成为新的尾节点?不对,插入在尾节点后面,新节点成为新的尾节点?不对,循环链表中,尾节点的下一个是头节点,所以插入到尾节点后面相当于在头节点前面插入,所以新节点成为新的头节点,而尾节点不变(因为尾节点还是指向新的头节点)?不对,尾节点应该指向新的头节点,而新节点插入在尾节点后面,那么尾节点的next变成了新节点,新节点的next指向原来的头节点,所以新节点成为新的头节点,而尾节点不变(因为尾节点还是指向新的头节点)?不对,尾节点应该指向头节点,现在头节点变了,所以尾节点的next应该指向新节点(新的头节点),所以尾节点不需要改变,因为新节点就是新的头节点,而尾节点指向它。 因此,如果我们维护一个尾指针tail,那么: 头插:新节点->next = tail->next; // 新节点指向原来的头节点 tail->next = newNode; // 尾节点指向新节点(新节点成为新的头节点) 这样,头插完成,而且不需要遍历,同时尾指针不变(因为头节点变了,但尾节点仍然指向新的头节点?不对,尾节点指向的是头节点,现在头节点变成了新节点,所以尾节点的next指向新节点,而新节点指向原来的头节点,所以链表结构正确)。而且,这样操作后,头节点变成了新节点,尾节点还是tail,但tail->next就是新节点(即头节点)。 但是,注意:我们维护的是尾指针,那么头节点就是tail->next。所以,如果我们只有尾指针,那么头插就是上面这样。而且,尾插也很方便:在tail后面插入新节点,然后更新tail为新节点即可。 所以,实际上,在循环链表中,使用尾指针表示整个链表可以方便头插和尾插。但是,用户要求的是“无头”,这里我们通常用一个头指针指向第一个节点,但如果我们用尾指针表示,那么头指针可以通过尾指针的next得到。所以,我们可以用一个指向尾节点的指针(称为tail)来表示链表。 然而,用户的问题中并没有指定使用头指针还是尾指针,我们通常可以用头指针。但为了高效,我们使用尾指针来表示循环链表(这样头尾操作都高效)。不过,为了简单,我们还是按照头指针的方式实现,并接受头插需要遍历的事实(因为教学目的,清晰为主)。 因此,我们继续使用头指针phead(指向第一个节点)来实现。 尾插法(使用头指针): void InsertBack(Node** phead, int data) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; if (*phead == NULL) { newNode->next = newNode; *phead = newNode; } else { // 找到尾节点 Node* tail = *phead; while (tail->next != *phead) { tail = tail->next; } // 插入在尾节点后面 newNode->next = *phead; // 新节点的next指向头节点(形成循环) tail->next = newNode; // 尾节点指向新节点 // 注意:头指针不变 } } 头删(使用头指针): void DeleteFront(Node** phead) { if (*phead == NULL) { return; // 空链表 } if ((*phead)->next == *phead) { // 只有一个节点 free(*phead); *phead = NULL; } else { // 找到尾节点 Node* tail = *phead; while (tail->next != *phead) { tail = tail->next; } Node* temp = *phead; // 要删除的头节点 *phead = (*phead)->next; // 头指针指向下一个节点(新的头节点) tail->next = *phead; // 尾节点指向新的头节点 free(temp); } } 尾删(使用头指针): void DeleteBack(Node** phead) { if (*phead == NULL) { return; } if ((*phead)->next == *phead) { free(*phead); *phead = NULL; } else { // 找到倒数第二个节点(即尾节点的前一个) Node* prev = *phead; while (prev->next->next != *phead) { prev = prev->next; } Node* tail = prev->next; // 尾节点 prev->next = *phead; // 倒数第二个节点指向头节点 free(tail); } } 查找(返回节点位置,即第几个节点,从1开始计数): int Find(Node* phead, int data) { if (phead == NULL) { return -1; // 没找到 } int pos = 1; Node* cur = phead; do { if (cur->data == data) { return pos; } cur = cur->next; pos++; } while (cur != phead); // 循环一圈回到起点 return -1; } 遍历(打印链表): void PrintList(Node* phead) { if (phead == NULL) { printf("空链表\n"); return; } Node* cur = phead; do { printf("%d ", cur->data); cur = cur->next; } while (cur != phead); printf("\n"); } 销毁链表: void DestroyList(Node** phead) { if (*phead == NULL) { return; } Node* cur = *phead; Node* next = NULL; // 先将尾节点的next置为NULL,变成非循环链表,然后释放 // 先找到尾节点,将尾节点的next置为NULL Node* tail = *phead; while (tail->next != *phead) { tail = tail->next; } tail->next = NULL; // 断开循环 // 现在按照非循环链表释放 cur = *phead; while (cur) { next = cur->next; free(cur); cur = next; } *phead = NULL; } 注意:以上代码中,头插、头删、尾删都需要遍历整个链表,效率较低。在实际应用中,如果频繁进行头插和尾插,建议使用双向链表,或者维护一个尾指针(这样头插和尾插都可以在O(1)时间内完成)。 下面我们给出一个使用尾指针的循环链表的简单示例(只给出结构,不详细实现): 我们用一个尾指针tail来表示链表,那么头节点就是tail->next。 头插:在tail->next(即头节点)前面插入,实际上就是在tail后面插入(因为循环链表),然后更新头节点(即tail->next)为新节点?不对,插入在tail后面,那么新节点成为尾节点?不对,插入在tail后面,新节点在tail和头节点之间,所以新节点成为新的头节点,而tail仍然指向原来的尾节点(但此时新节点在尾节点后面,所以尾节点应该是新节点?不对,我们插入在尾节点tail的后面,所以新节点成为新的尾节点?不对,循环链表插入在A节点后面,就是A->next指向新节点,新节点->next指向A原来的下一个节点。所以,插入在tail后面,新节点的next指向tail->next(即头节点),而tail->next指向新节点。这样,新节点就位于tail(尾节点)和头节点之间,即新节点成为新的尾节点?不对,尾节点应该是最后一个节点,而新节点后面是头节点,所以新节点是尾节点,而原来的尾节点不再是尾节点。所以,我们需要更新tail指向新节点。 因此,头插操作(在头节点前面插入)使用尾指针: newNode->next = tail->next; // 新节点指向头节点 tail->next = newNode; // 尾节点指向新节点 // 这样新节点就成为了新的头节点,但尾节点没有变(因为插入在尾节点后面,所以尾节点还是原来的尾节点,但它的下一个节点变成了新节点,而新节点指向原来的头节点,所以链表结构正确,但尾节点并没有更新,因为新节点并不是尾节点,而是头节点?不对,在循环链表中,尾节点应该是最后一个节点,而新节点插入在尾节点后面,那么新节点应该成为新的尾节点。所以我们需要更新tail为newNode。 // 所以,头插操作实际上变成了尾插?不对,我们是在头节点前面插入,但使用尾指针,我们是在尾节点后面插入,而尾节点后面就是头节点,所以在头节点前面插入等同于在尾节点后面插入,而插入后新节点成为新的尾节点(因为它在尾节点后面,并且连接着头节点)。所以,头插操作后,头节点变成了新节点,而尾节点也变成了新节点。因此,我们需要更新tail为newNode。 因此,头插操作(使用尾指针): newNode->next = tail->next; tail->next = newNode; tail = newNode; // 更新尾指针 尾插操作(在链表末尾插入): 同样在尾节点后面插入,然后更新尾指针为新节点,所以操作和头插一样?不对,尾插就是在尾节点后面插入,然后更新尾指针为新节点,所以操作和上面一样。所以,头插和尾插操作完全一样?这显然不对。 实际上,我们混淆了头插和尾插的位置。在循环链表中,尾指针指向的是链表的最后一个节点,而最后一个节点的next就是头节点。所以: 头插:在头节点前面插入,即作为新的头节点,同时因为新节点插入在尾节点后面(因为尾节点的next是头节点,所以头节点前面就是尾节点后面),所以插入在尾节点后面,然后更新尾指针指向新节点(因为新节点成为新的尾节点?不对,新节点成为新的头节点,而原来的尾节点仍然指向它,所以尾节点不变?不对,插入在尾节点后面,新节点成为尾节点?不对,插入后,新节点位于尾节点和头节点之间,那么新节点成为新的尾节点,而原来的尾节点不再是尾节点?不对,原来的尾节点还是尾节点,因为它后面是新节点,而新节点后面是头节点,所以新节点在最后,所以新节点成为新的尾节点。所以,我们需要更新尾指针。 因此,头插操作(在头节点前面插入)实际上导致新节点成为新的尾节点?这显然不合理。因为头插应该是在头部插入,新节点成为头节点,而原来的头节点成为第二个节点,尾节点不变。 正确理解:在循环链表中,尾节点tail,头节点head=tail->next。 头插:我们希望新节点成为新的头节点,而原来的头节点变成第二个节点,尾节点不变(因为头插并不影响尾节点,除了尾节点的next需要指向新头节点)。 - 新节点插入在尾节点和头节点之间:即尾节点后面,头节点前面。 - 操作:newNode->next = tail->next; // 新节点指向头节点 tail->next = newNode; // 尾节点指向新节点 - 这样,新节点就成为了头节点(因为tail->next指向它),而原来的头节点成为第二个节点。尾节点tail不变,因为它仍然指向新的头节点(通过tail->next)。 - 所以,头插后,头节点变为新节点,尾节点不变。 尾插:我们希望新节点成为新的尾节点,原来的尾节点成为倒数第二个节点,头节点不变。 - 操作:同样在尾节点后面插入新节点(因为尾节点后面就是头节点,所以插入后新节点在尾节点和头节点之间),然后更新尾指针为新节点。 - 具体: newNode->next = tail->next; // 新节点指向头节点 tail->next = newNode; // 尾节点指向新节点 tail = newNode; // 更新尾指针为新节点 - 这样,新节点成为新的尾节点,原来的尾节点不再是尾节点,而头节点不变。 所以,头插和尾插的区别在于:头插不更新尾指针,尾插需要更新尾指针。 因此,使用尾指针的循环链表: 头插:不改变尾指针,只改变尾指针的next指向新节点(新节点成为头节点)。 尾插:除了改变尾指针的next,还要更新尾指针为新节点。 但是,头插操作后,链表的结构发生了变化,头节点变了,但尾节点没变,所以尾节点tail的next指向了新节点(头节点),而新节点的next指向原来的头节点。这样结构正确。 所以,头插函数(使用尾指针tail): void InsertFront(Node** ptail, int data) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; if (*ptail == NULL) { // 空链表 newNode->next = newNode; *ptail = newNode; } else { newNode->next = (*ptail)->next; (*ptail)->next = newNode; // 不更新尾指针,因为尾指针不变 } } 尾插函数: void InsertBack(Node** ptail, int data) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; if (*ptail == NULL) { newNode->next = newNode; *ptail = newNode; } else { newNode->next = (*ptail)->next; (*ptail)->next = newNode; *ptail = newNode; // 更新尾指针 } } 头删(使用尾指针): 删除头节点(即tail->next)。 注意:如果链表只有一个节点,删除后链表为空。 否则,删除头节点,并更新tail->next指向新的头节点(即原头节点的下一个节点),同时如果删除的节点是尾节点(即链表只有一个节点),那么需要将tail置为NULL,否则tail不变。 具体: void DeleteFront(Node** ptail) { if (*ptail == NULL) { return; } if ((*ptail)->next == *ptail) { // 只有一个节点 free(*ptail); *ptail = NULL; } else { Node* head = (*ptail)->next; // 头节点 (*ptail)->next = head->next; // 跳过头节点 free(head); // 尾指针不变,因为删除头节点不影响尾节点(除只有一个节点) } } 尾删(使用尾指针): 删除尾节点,这需要找到尾节点的前一个节点(因为单链表,我们需要将前一个节点的next指向头节点,并更新尾指针为前一个节点)。 但是,使用尾指针,我们无法直接得到前一个节点,所以需要遍历?这违背了使用尾指针的初衷。 所以,使用尾指针的循环链表,尾删操作需要遍历,效率不高。因此,如果经常需要尾删,建议使用双向链表。 综上所述,单向循环链表使用头指针实现时,头插、头删、尾插、尾删都可能需要遍历(头插、尾插、尾删需要遍历找尾,头删需要遍历找尾来更新尾节点的next)。使用尾指针实现时,头插、尾插可以在O(1)时间内完成,头删也可以O(1),但尾删需要遍历。 因此,根据用户的问题,我们选择使用头指针实现,并接受遍历的开销(因为教学目的,代码清晰)。 下面,我们给出完整的单向无头循环链表的实现(使用头指针)的代码框架: 注意:以下代码为示例,未经过完整测试,主要用于展示原理。</think>### 单向无头循环链表的实现原理及操作方法 单向无头循环链表是一种特殊的链表结构,具有以下特点: 1. **无头结点**:链表的第一个节点就是数据节点 2. **单向性**:每个节点只包含指向后继节点的指针 3. **循环性**:尾节点的指针指向首节点(形成闭环) 4. **自我管理**:不需要额外的头节点进行管理 #### 数据结构定义 ```c typedef struct Node { int data; // 数据域 struct Node* next; // 指针域(指向后继节点) } Node; // 链表管理指针(指向首节点) Node* list = NULL; // 空链表时 list == NULL ``` #### 核心操作实现 ##### 1. 初始化链表 ```c void InitList(Node** pphead) { *pphead = NULL; // 初始化链表指针为空 } ``` ##### 2. 插入操作 **头插法**(时间复杂度 $O(n)$): ```c void InsertFront(Node** pphead, int data) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; if (*pphead == NULL) { // 空链表:自循环 newNode->next = newNode; *pphead = newNode; } else { // 找到尾节点(尾节点的next指向首节点) Node* tail = *pphead; while (tail->next != *pphead) { tail = tail->next; } // 插入新节点 newNode->next = *pphead; tail->next = newNode; *pphead = newNode; // 更新首节点 } } ``` **尾插法**(时间复杂度 $O(n)$): ```c void InsertBack(Node** pphead, int data) { Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; if (*pphead == NULL) { newNode->next = newNode; *pphead = newNode; } else { Node* tail = *pphead; while (tail->next != *pphead) { tail = tail->next; } newNode->next = *pphead; // 新节点指向首节点 tail->next = newNode; // 原尾节点指向新节点 } } ``` ##### 3. 删除操作 **头删法**: ```c void DeleteFront(Node** pphead) { if (*pphead == NULL) return; if ((*pphead)->next == *pphead) { // 单节点链表 free(*pphead); *pphead = NULL; } else { Node* tail = *pphead; while (tail->next != *pphead) { tail = tail->next; } Node* temp = *pphead; // 保存原首节点 *pphead = (*pphead)->next; // 更新首节点 tail->next = *pphead; // 尾节点指向新首节点 free(temp); // 释放原首节点 } } ``` **尾删法**: ```c void DeleteBack(Node** pphead) { if (*pphead == NULL) return; if ((*pphead)->next == *pphead) { free(*pphead); *pphead = NULL; } else { Node* prev = NULL; Node* curr = *pphead; // 定位尾节点及其前驱 while (curr->next != *pphead) { prev = curr; curr = curr->next; } prev->next = *pphead; // 前驱指向首节点 free(curr); // 释放尾节点 } } ``` ##### 4. 查找操作 ```c Node* Find(Node* phead, int target) { if (phead == NULL) return NULL; Node* curr = phead; do { if (curr->data == target) return curr; curr = curr->next; } while (curr != phead); // 循环终止条件 return NULL; // 未找到 } ``` ##### 5. 遍历链表 ```c void Traverse(Node* phead) { if (phead == NULL) return; Node* curr = phead; do { printf("%d ", curr->data); curr = curr->next; } while (curr != phead); printf("\n"); } ``` ##### 6. 销毁链表 ```c void DestroyList(Node** pphead) { if (*pphead == NULL) return; Node* curr = *pphead; Node* next = NULL; // 先断开循环 Node* tail = *pphead; while (tail->next != *pphead) { tail = tail->next; } tail->next = NULL; // 断开循环链 // 释放节点 while (curr != NULL) { next = curr->next; free(curr); curr = next; } *pphead = NULL; // 重置链表指针 } ``` #### 操作特性分析 | 操作 | 时间复杂度 | 注意事项 | |------------|------------|------------------------------| | **头插** | $O(n)$ | 需要遍历找尾节点 | | **尾插** | $O(n)$ | 需要遍历找尾节点 | | **头删** | $O(n)$ | 需要更新尾节点的指针 | | **尾删** | $O(n)$ | 需要定位倒数第二个节点 | | **查找** | $O(n)$ | 循环终止条件为回到起始点 | | **遍历** | $O(n)$ | 使用do-while保证至少执行一次 | | **销毁** | $O(n)$ | 需先断开循环再释放 | #### 应用场景 1. 周期性任务调度(如轮询系统) 2. 游戏开发中的循环角色队列 3. 资源循环分配系统 4. 需要周期性遍历的场景(如缓冲区管理) > 关键优势:尾节点直接连接首节点,省去遍历找尾的过程,适合需要循环访问的场景[^1]。但单链表结构在删除操作时仍需遍历找前驱节点,这是其效率瓶颈[^2]。 --- ### 相关问题 1. 如何优化单向循环链表的删除操作效率? 2. 单向循环链表双向循环链表的主要区别是什么? 3. 在什么场景下应该优先选择循环链表普通链表? 4. 如何实现约瑟夫环问题(Josephus problem)使用循环链表? 5. 循环链表在内存管理中的典型应用有哪些? [^1]: C语言实现无头单向不循环链表 [^2]: 【数据结构链表无头单向非循环链表实现
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值