LinkedList与链表

一、链表

链表是一种物理结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。逻辑上是连续的

2)链表结构(8种)

单向/双向+带头/不带头+循环/非循环(组合)

在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。


二、链表的实现

1.有头单向非循环链表实现

代码如下(示例):

package demo2;

public class MySingleList {
    static class ListNode{
        public int val;//结点的值
        public ListNode next;//下一个节点的地址 (引用类型)

        public ListNode(int val) {
            this.val = val;
        }
    }
        public  ListNode head;//表示当前链表的头节点
    //创建一个链表
      public void createList() {
          //构造方法

          ListNode node1=new ListNode(12);
          ListNode node2=new ListNode(23);
          ListNode node3=new ListNode(34);
          ListNode node4=new ListNode(45);
          ListNode node5=new ListNode(56);
          //以链表的方式连接起来
          node1.next=node2;
          node2.next=node3;
          node3.next=node4;
          node4.next=node5;
          this.head=node1;


      }
    public void display() {
//          while (head!=null){
//              System.out.print(head.val+" ");
//              head=head.next;
//          }
//          //换行
//        System.out.println();
//    }
         ListNode cur=head;
        while (cur!=null){
            System.out.print(cur.val+" ");
            //cur=null证明链表遍历完成了
            cur=cur.next;
        }
        //换行
        System.out.println();
    }




    //头插法
    public void addFirst(int data){
          //在插入的时候先绑定后面节点的信息
        ListNode node=new ListNode(data);
        node.next=head;
        head=node;
    }
    //尾插法
    public void addLast(int data){
        ListNode node=new ListNode(data);
        ListNode cur=head;
        //cur/head==null cur.next空指针异常
        if (cur==null){
            head=node;
            return;
        }
        //cur.next==null说明cur所指向的节点是尾巴节点
        //cur==null代表把链表的每个节点都遍历完了
        while (cur.next!=null){
            cur=cur.next;
            }
        cur.next=node;
    }
    //任意位置插入,第一个数据节点为0号下标
    public void addIndex(int index,int data){
          if(index<0||index>size()){
              System.out.println("index 不合法");
              return;
          }
          if(index==0){
              //头插法
              addFirst(data);
              return;
          }
          if (index==size()){
              //尾插法
              addLast(data);
              return;
          }
          //定义cur走index-1步,找到插入位置的前一个插入
        ListNode cur=findIndexSubOne(index);
          ListNode node=new ListNode(data);
          node.next=cur.next;
          cur.next=node;

    }
    //要找节点的前一个节点
    private ListNode findIndexSubOne(int index){
          ListNode cur=head;
          int count=index-1;
          while (count!=0){
              cur=cur.next;
              count--;
          }
          return cur;
    }
    //查找是否包含关键字key是否在单链表当中
    public boolean contains(int key){
        ListNode cur=head;
        while (cur!=null){
           if(cur.val==key){
               return true;
           }
           cur=cur.next;
        }
        return false;
    }
    //删除第一次出现关键字为key的节点
    public void remove(int key){
          if(head==null){
              return;
          }
          if(head.val==key){
              head=head.next;
              return;
          }
         ListNode cur=searchPrev(key);
         if (cur==null){
             System.out.println("没有你要删除的数字");
             return;
         }
         ListNode del=cur.next;
         cur.next=del.next;
    }
    //要删除值的节点的前一个节点
    private ListNode searchPrev(int key){
        ListNode cur=head;
        while (cur!=null){
            if(cur.next.val==key){
                return cur;
            }
            cur=cur.next;
        }
        return null;
    }

    //删除所有值为key的节点
    public void removeAllKey(int key){
          if (head==null){
              return;
          }
          ListNode cur=head.next;
          ListNode prev=head;
          while (cur!=null){
              if(cur.val==key){
                  prev.next=cur.next;
                  cur=cur.next;

              }else {
                  prev=cur;
                  cur=cur.next;
              }

          }
          if(head.val==key){
              head=head.next;
          }
    }
    //得到单链表的长度
    public int size(){
          int count=0;
        ListNode cur=head;
        while (cur!=null){
            count++;
            cur=cur.next;
        }
        return count;
    }
    public void clear() {
          this.head=null;
    }

}
class Test{
    public static void main(String[] args) {
        MySingleList mySingleList=new MySingleList();
        mySingleList.createList();
        mySingleList.display();
        System.out.println("2.===========");
        //链表长度
        System.out.println(mySingleList.size());
        //判断123是否在单链表中
        System.out.println(mySingleList.contains(123));
        System.out.println("头插999");
        mySingleList.addFirst(999);
        mySingleList.display();
        System.out.println("尾插888");
        mySingleList.addLast(888);
        mySingleList.display();
        System.out.println("1位置插入7");
        mySingleList.addIndex(1,7);
        mySingleList.display();
        System.out.println("删除999");
        mySingleList.remove(999);
        mySingleList.display();
        System.out.println("删除所有的12");
        mySingleList.addFirst(12);
        mySingleList.display();
        mySingleList.removeAllKey(12);
        mySingleList.display();
        System.out.println("删除所有节点");
        mySingleList.clear();
        System.out.println(mySingleList.size());


    }
}

 

2.代码解析

1)打印 :这种方法打印最后头节点为空(打印后找不到头节点)  //创建临时节点cur

2)任意位置插入元素

被插入位置的前一个节点cur

node.next=cur.next;

cur.next=node;

3)删除节点

找到要删除节点的前一个节点

删除 cur.next=del.next

4)常用方法

boolean add(E e) 尾插 e

void add(int index, E element) 将 e 插入到 index 位置

boolean addAll(Collection c) 尾插 c 中的元素 E

remove(int index) 删除 index 位置元素

boolean remove(Object o) 删除遇到的第一个 o

E get(int index) 获取下标 index 位置元素 E

set(int index, E element) 将下标 index 位置元素设置为 element

void clear() 清空

boolean contains(Object o) 判断 o 是否在线性表中

int indexOf(Object o) 返回第一个 o 所在下标

int lastIndexOf(Object o) 返回最后一个 o 的下标

List subList(int fromIndex, int toIndex) 截取部分 list


三、ArrayList和LinkedList的区别 

不同点    ArrayList                                       LinkedList

存储空间上    物理上一定连续                     逻辑上连续,但物理上不一定连续

随机访问    支持O(1)                                   不支持:O(N)

头插    需要搬移元素,效率低O(N)             只需修改引用的指向,时间复杂度为O(1)

插入     空间不够时需要扩容                        没有容量的概念

应用场景    元素高效存储+频繁访问            任意位置插入和删除频繁

LinkedList vs ArrayList

内部实现
  • LinkedList:基于双向链表实现。每个元素(节点)包含对前一个和后一个节点的引用。这使得LinkedList在插入和删除操作时不需要移动其他元素,因此在列表中间执行这些操作效率更高。
  • ArrayList:基于动态数组实现。它提供了对元素的随机访问能力,因为可以通过索引直接访问元素。然而,在进行插入或删除操作(特别是列表中部的操作)时,可能需要移动元素以保持连续性,这会带来额外的时间开销。
性能特点
  • LinkedList
    • 插入和删除操作(尤其是两端)效率高,时间复杂度为O(1)。
    • 随机访问元素效率低,时间复杂度为O(n),因为需要从头或尾遍历到指定位置。
  • ArrayList
    • 支持快速随机访问,时间复杂度为O(1)。
    • 插入和删除操作(尤其是在列表中部)效率较低,时间复杂度为O(n),因为需要移动元素。
适用场景
  • LinkedList:适用于频繁进行插入和删除操作的场景,如实现栈和队列等数据结构。
  • ArrayList:适用于需要频繁访问元素,而插入和删除操作相对较少的场景。
<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]: 【数据结构链表及无头单向非循环链表实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值