关于列表循环删除自身元素的问题

本文探讨了在Python中从列表中安全删除元素的方法。避免在遍历列表时直接删除元素,而是推荐通过创建辅助列表的方式进行操作。此外,还介绍了如何使用集合(set)来去除重复项并保持列表原有顺序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于列表循环删除自身元素的问题

不能在循环自身的时候删除自身元素!!!

例:我想要删除a与start和end中共有的元素

a = ['2021-07-01', '2021-07-02', '2021-07-03', '2021-07-04', '2021-07-05',
 '2021-07-06', '2021-07-07', '2021-07-08', '2021-07-09', '2021-07-10',
  '2021-07-11', '2021-07-12', '2021-07-13', '2021-07-14', '2021-07-15',
   '2021-07-16', '2021-07-17', '2021-07-18']

start = ['2021-06-28', '2021-06-29', '2021-06-30', '2021-07-01', '2021-07-02', 
'2021-07-03', '2021-07-04']

end = ['2021-07-12', '2021-07-13', '2021-07-14', '2021-07-15', '2021-07-16', 
'2021-07-17', '2021-07-18']

错误:自身循环删除自身元素

for i in a:
    if i in start or i in end:
        a.remove(i)

print(a)
#
['2021-07-02', '2021-07-04', '2021-07-05', '2021-07-06', '2021-07-07',
 '2021-07-08', '2021-07-09', '2021-07-10', '2021-07-11', '2021-07-13', 
 '2021-07-15', '2021-07-17']

可以看到,删除的元素乱七八糟,没有实现想要的效果

错误原因:

因为列表的元素在内存中是一段连续的物理地址,如果删除后,地址就会往前移。导致地址删除的元素对象发生变化。而在自身循环中,获取的自身元素信息是在删除之前,导致删除后并没有及时获取自身元素对应的新内存,从上面错误结果可以看见,其实是删除一个然后跳过了一个。所以删除不要在自身循环中去删除

print('列表地址:',id(a))

id_list = [id(i) for i in a]
print(id_list)
print()
for i in a:
    if i in start or i in end:
        a.remove(i)
        new_id_list = [id(i) for i in a]
        print(new_id_list)




列表地址: 2036980998728
[2036981497328, 2036985102896, 2036985073008, 2036985070192, 2036985072304, 2036985072496, 2036985072816, 2036985072944, 2036985070320, 2036985144368, 2036985144432, 2036985144496, 2036985144560, 2036985144624, 2036985144688, 2036985144752, 2036985144816, 2036985144880]

[2036985102896, 2036985073008, 2036985070192, 2036985072304, 2036985072496, 2036985072816, 2036985072944, 2036985070320, 2036985144368, 2036985144432, 2036985144496, 2036985144560, 2036985144624, 2036985144688, 2036985144752, 2036985144816, 2036985144880]
[2036985102896, 2036985070192, 2036985072304, 2036985072496, 2036985072816, 2036985072944, 2036985070320, 2036985144368, 2036985144432, 2036985144496, 2036985144560, 2036985144624, 2036985144688, 2036985144752, 2036985144816, 2036985144880]
[2036985102896, 2036985070192, 2036985072304, 2036985072496, 2036985072816, 2036985072944, 2036985070320, 2036985144368, 2036985144432, 2036985144560, 2036985144624, 2036985144688, 2036985144752, 2036985144816, 2036985144880]
[2036985102896, 2036985070192, 2036985072304, 2036985072496, 2036985072816, 2036985072944, 2036985070320, 2036985144368, 2036985144432, 2036985144560, 2036985144688, 2036985144752, 2036985144816, 2036985144880]
[2036985102896, 2036985070192, 2036985072304, 2036985072496, 2036985072816, 2036985072944, 2036985070320, 2036985144368, 2036985144432, 2036985144560, 2036985144688, 2036985144816, 2036985144880]
[2036985102896, 2036985070192, 2036985072304, 2036985072496, 2036985072816, 2036985072944, 2036985070320, 2036985144368, 2036985144432, 2036985144560, 2036985144688, 2036985144816]

正确:别处循环删除某列表元素

因为从别的循环中去删除列表元素,每次循环都是重新读取该列表新的元素内存地址,所有可以放心删除。

for i in start:
    if i in a:
        a.remove(i)
        
for o in end:
    if o in a:
        a.remove(o)
        
print(a)
#['2021-07-05', '2021-07-06', '2021-07-07', '2021-07-08', '2021-07-09',
# '2021-07-10', '2021-07-11']

当然也可以用set

但是set不能把保证还是原来的顺序,需要根据情况来使用

r = set(a) -set(start) -set(end)
print(sorted(list(r)))
#['2021-07-05', '2021-07-06', '2021-07-07', '2021-07-08', '2021-07-09',
# '2021-07-10', '2021-07-11']
<think>题目要求实现循环链表的元素插入。循环链表的特点是最后一个节点的next指针指向头节点。我们将实现一个简单的循环链表,并支持在任意位置插入元素。注意:循环链表需要特别处理插入位置为0(头节点位置)的情况,以及链表为空的情况。设计思路:1.定义链表节点结构体,包含数据域和指向下一个节点的指针。2.定义循环链表类,包含头指针以及相关的成员函数。3.实现插入函数,考虑插入位置(从0开始计数)的有效性(即位置不能大于链表长度,小于0;但可以等于链表长度,此时相当于在尾部插入)。由于是循环链表,我们还需要维护循环特性。插入操作需要考虑的情况:a.链表为空(插入位置只能为0)b.插入位置为0(即在头节点之前插入,成为新的头节点)c.插入位置在中间或末尾注意:在循环链表中,当链表非空时,最后一个节点的next指向头节点。我们将实现一个循环链表类,包含:-节点结构体-构造函数和析构函数(释放链表)-插入函数:insert(intdata,intposition)-打印函数:用于验证注意:在插入时,如果插入位置大于链表长度,则不合法。但题目没有明确,我们这里按可以等于链表长度处理(即插入到最后一个节点之后)。如果插入位置等于0,表示插入到第一个节点之前(成为新的头节点)。另外,我们也可以维护一个尾指针来简化插入操作,但这里为了通用性,不使用尾指针,而是遍历找到指定位置。具体步骤(插入操作):1.如果链表为空,则新节点成为头节点,且其next指向自身。2.如果链表非空:如果插入位置为0:创建新节点,将其next指向当前头节点。然后需要找到尾节点(因为尾节点当前指向原来的头节点),将其next指向新的头节点。然后更新头节点为新节点。如果插入位置不为0:遍历到插入位置的前一个节点(比如要在位置i插入,则找到位置i-1的节点)。将新节点的next指向当前节点的next。当前节点的next指向新节点。注意:如果插入位置是最后一个节点之后(即position等于链表长度),那么新节点成为最后一个节点,其next指向头节点(循环)。然而,在非0位置插入,还有一种更简单的做法:由于循环链表非空时尾节点指向头节点,所以我们不需要像处理位置0那样更新尾节点,只需正常地插入节点。但是,当插入位置为0时,我们必须更新尾节点指向新的头节点,因为原来的尾节点指向原来的头节点,现在头节点变了。或者,我们可以避免每次在头部插入时都遍历到尾节点,可以采用以下方式:非空链表中插入位置0:步骤1:创建新节点,将其next指向当前头节点。步骤2:找到尾节点,将尾节点的next指向新节点(这一步需要遍历整个链表,所以效率较低)。步骤3:将头指针指向新节点。另一种方法是:在非空链表中插入位置0时,先插入新节点,但插入方式是在头节点之后插入新节点,然后交换两个节点的数据,这样就不需要更新尾节点了。但是这样数据顺序会乱,不推荐。我们还可以用一个方法:在创建节点时,先不关心循环,插入完成后再调整。但在位置0插入后,我们需要让新的头节点成为链表的开始,同时尾节点指向它。为了完整展示,我们按直接处理的方式。考虑优化:维护尾指针,这样可以快速找到尾节点。但题目要求手写代码,没有特殊要求,我们这里先实现简单版本,即每次在头部插入时遍历到尾节点。由于在头部插入需要O(n)时间,而其他位置插入也需要最多O(n)时间(因为需要找到插入位置的前一个节点),所以整体是O(n)的。另一种思路:我们可以实现循环链表时使用一个指针指向尾节点,这样在头部插入时可以直接通过尾节点快速找到头节点(因为尾节点->next就是头节点),然后更新尾节点->next为新节点,同时将头节点指向新节点,这样就不需要遍历了。但是,这需要额外的尾指针成员,而且需要更新尾指针(如果插入位置是尾部则更新尾指针)。由于题目没有明确要求,我们这里选择不维护尾指针,但会说明维护尾指针的好处。实现步骤(不使用尾指针):插入操作:1.创建新节点。2.如果链表为空:new_node->next=new_node;//循环head=new_node;3.否则,如果位置为0:new_node->next=head;然后需要找到尾节点(从头节点开始遍历,直到某个节点的next指向头节点),将尾节点的next指向new_node。更新head=new_node;//新节点成为头节点4.否则(位置不为0):我们遍历到指定位置的前一个节点(current)。注意:位置从0开始,所以当前节点从head开始,然后走position-1步。如果位置大于长度怎么办?我们可以先获取长度,然后如果位置大于长度,则视为无效(或者根据题目要求,选择插入到尾部?)。但题目没有说,我们假设位置是合法的(0到长度之间)。为了健壮,当位置大于长度时,我们将位置设定为末尾(即位置等于当前长度)。或者也可以直接返回错误。这里我们选择支持插入到末尾之后,也就是视为在末尾插入。因此,我们需要先计算链表的长度,然后对位置进行修正(如果位置>长度,则置为长度?)或者遍历时判断。注意:循环链表遍历时,我们有一个头节点,所以遍历需要小心死循环。我们可以用一个计数器,直到再次遇到头节点(但这样不对,因为循环链表节点都是连通的,所以一般使用计数的方式,从head开始,逐个计数,直到再次遇到head?不对,应该是非循环链表那样遍历,但结束条件是指定位置,然后注意循环结束条件。由于我们插入操作需要知道链表长度吗?不,我们可以这样:从head开始,遍历position次,会超出范围?实际上,我们需要找的是插入位置的前一个节点,如果position等于0,已经单独处理;如果position大于0,那么我们需要找的是位置为position-1的节点。例如:链表:1->2->3->(回到1),长度为3。要在位置1插入(在1和2之间):需要找到位置0的节点(即1),然后在1后面插入。要在位置3插入(相当于在3之后插入):即position=3(节点从0,1,2对应节点1,2,3),那么需要找到位置2的节点(节点3),然后插入。但是,如果位置为4呢?按照循环链表,位置3的next是头节点,所以位置4相当于位置0(因为循环)?但一般插入操作不允许位置大于长度(这里长度是3),位置应该从0到3(3表示在最后一个节点之后,新节点成为最后一个节点)。所以我们规定:插入位置可以是0到长度(包括)之间的任意位置。如果位置小于0,我们将其设置为0;如果位置大于长度,我们将其设置为长度。因此,在插入位置不为0的情况下,我们需要:如果位置大于0且小于等于长度,则找到位置的前一个节点(位置为i,则找第i-1个节点)。但如果位置为0,我们已经单独处理了。步骤:计算链表长度(len):如果head==nullptr,则长度为0。否则,从head开始计数,直到再次遇到head(这里不能用再次遇到head作为结束,因为循环链表的结束条件应该是走到头节点就停吗?不,我们一般用计数的方式,从head开始,计数一次,然后走到下一个节点,直到回到head,但实际上循环链表遍历一圈应该就停。但是,计算长度需要遍历整个链表,而插入操作又要遍历到前一个节点,相当于遍历两次。我们可以优化:在遍历找前一个节点的时候同时计数,如果计数值等于长度(此时已经处理了插入位置在最后的情况),就停止。另一种方法是:不预先计算长度,而是直接遍历到插入位置的前一个节点,同时计数,当达到position-1时停止。但如果插入位置大于链表长度,我们需要处理为插入到末尾。这样我们遍历时需要一个计数器,直到到达前一个节点,如果中途到达了头节点(说明已经遍历了一圈),那么我们就停止并插入在末尾(即当前遍历的最后一个节点)?但是,由于我们是循环链表,我们无法通过判断是否回到头节点来停止,因为一开始就从头节点开始的。所以我们需要先知道长度,然后再确定是否位置超出长度。因此,我们先计算链表的长度。为了简洁,我们这样设计:如果链表为空,则位置只能为0。如果不为空:先计算链表长度(len),然后对位置进行修正:如果position<0,则position=0;如果position>len,则position=len;//注意,位置0~len有效然后:if(position==0){//特殊处理}else{//然后我们需要找位置为position-1的节点Node*temp=head;for(inti=0;i< position-1;i++){//注意:这里走position-1步temp=temp->next;}new_node->next=temp->next;temp->next=new_node;//注意:如果是插入到最后一个节点之后(position等于len),那么new_node的next指向temp->next(原来是头节点),所以自然形成循环,不需要额外操作。}在位置0插入的代码:Node*new_node=newNode(data);if(head==nullptr){new_node->next=new_node;head=new_node;}else{new_node->next=head;//找到尾节点Node*tail=head;while(tail->next!=head){//注意:循环链表,尾节点的next指向headtail=tail->next;}tail->next=new_node;head=new_node;//新节点成为头}但是,在非0位置插入时,如果链表原本长度为1,那么在位置1(即末尾)插入:position=1,修正后position=1(在0和1之间没有节点,所以要找位置0的节点,即头节点)for循环:i从0到0(i<0)不执行,所以temp就是head。new_node->next=temp->next;即指向原head的next(即自己?不对,原本只有一个节点时,head->next=head(循环))所以new_node->next=head,然后temp->next=new_node,这样变为:head指向new_node,new_node指向head,这样就有两个节点,且head->next=new_node,new_node->next=head,正确。注意:在非0位置插入后,如果是插入在末尾,那么new_node->next就是head(因为temp->next原来是head,现在被new_node替代,而new_node的next指向head),所以循环是保持的。因此,代码逻辑是可行的。下面用C++代码实现:注意:由于是循环链表,析构函数需要释放内存,否则会内存泄漏。但是,我们这里重点在插入,所以析构函数可以省略(实际使用时需要实现)。为了验证,我们实现一个打印链表的函数,打印两圈(为了展示循环)?但这样可能会死循环。我们改为按照节点个数打印一圈。我们将实现:classCircularLinkedList包含:private:Node*head;public:CircularLinkedList():head(nullptr){}~CircularLinkedList();//释放所有节点,防止内存泄漏voidinsert(intdata,intposition);voidprintList()const;析构函数的实现:需要删除所有节点。由于循环链表,我们不能用简单的遍历方式(因为每个节点都有next指向,无法用nullptr判断结束)。所以需要先断开环,变为单链表再删除?或者用计数的方式,用额外的set存储已经访问的节点?这里我们采用:如果链表为空,不处理。否则,用一个指针current从head开始,用一个临时指针保存next,然后删除当前节点,然后移动到next,直到回到头节点。但是,删除头节点后,后面节点的next可能已经失效,所以我们需要先保存下一个节点,然后删除头节点,然后从下一个节点开始,直到又遇到头节点(但是头节点已经被删除了)?这样不行。另一种方法:用两个指针,依次删除节点。因为循环链表的节点个数是固定的,我们可以先计算节点个数,然后再按个数删除。或者:Node*current=head;Node*next=nullptr;while(current!=nullptr){next=current->next;//如果是最后一个节点,需要判断(当节点个数为1时,next等于head,但此时已经删除了head,所以需要先保存next再删除current)if(next==head){//说明已经只剩最后一个节点了deletecurrent;head=nullptr;break;}deletecurrent;current=next;}但是,这种方法在多个节点时:第一次删除头节点,然后current变成next(即第二个节点),此时头节点被删除了,但第二个节点的next指向的是原来的头节点(已经被删除),访问非法。正确的做法是:记录起始点(头节点),然后遍历直到回到起始点。if(head==nullptr)return;Node*current=head;do{Node*next=current->next;deletecurrent;current=next;}while(current!=head);//但是最后一个节点删除后,current变成head,但此时head已经被删除了?不对,因为当删除最后一个节点时,current=next(即head),然后循环条件不满足(因为current等于head,但head已经被删除,访问野指针)?所以不能这样做。可以采用计数的方式:intlen=length();//先计算长度Node*current=head;for(inti=0;i<len;i++){Node*next=current->next;deletecurrent;current=next;}head=nullptr;这样就不会形成死循环。我们先实现一个计算长度的函数:intlength()const{if(head==nullptr)return0;Node*temp=head;intcount=0;do{count++;temp=temp->next;}while(temp!=head);returncount;}然后析构函数就可以用计数来删除。下面开始写完整的代码:</think> ### 手写循环链表插入元素实现 在循环链表中插入元素需要特别注意链表循环特性的维护。我将实现一个完整的循环链表类,并详解插入操作的步骤。 ```cpp #include <iostream> using namespace std; // 链表节点结构 struct Node { int data; Node* next; Node(int val) : data(val), next(nullptr) {} }; // 循环链表类 class CircularLinkedList { private: Node* head; // 头节点指针 int size; // 链表大小 public: // 构造函数 CircularLinkedList() : head(nullptr), size(0) {} // 析构函数:释放所有节点 ~CircularLinkedList() { if (!head) return; Node* current = head; do { Node* temp = current; current = current->next; delete temp; } while (current != head); } // 插入函数 void insert(int data, int position) { // 位置有效性检查 if (position < 0 || position > size) { cout << "Invalid position: " << position << endl; return; } Node* newNode = new Node(data); size++; // 更新链表大小 // 空链表插入 if (!head) { head = newNode; head->next = head; // 循环指向自己 return; } // 头部插入 if (position == 0) { Node* tail = head; while (tail->next != head) { tail = tail->next; } newNode->next = head; head = newNode; tail->next = head; // 维护尾节点的循环指向 return; } // 中间或尾部插入 Node* current = head; // 移动到插入位置的前一个节点 for (int i = 0; i < position - 1; i++) { current = current->next; } newNode->next = current->next; current->next = newNode; } // 打印链表 void printList() { if (!head) { cout << "Empty List" << endl; return; } Node* current = head; cout << "Circular List: "; do { cout << current->data << " → "; current = current->next; } while (current != head); cout << "[HEAD]" << endl; } }; // 测试代码 int main() { CircularLinkedList list; // 测试插入操作 cout << "Inserting 10 at position 0:" << endl; list.insert(10, 0); list.printList(); // 10 cout << "\nInserting 20 at position 0:" << endl; list.insert(20, 0); list.printList(); // 20 → 10 cout << "\nInserting 30 at position 1:" << endl; list.insert(30, 1); list.printList(); // 20 → 30 → 10 cout << "\nInserting 40 at position 3 (end):" << endl; list.insert(40, 3); list.printList(); // 20 → 30 → 10 → 40 cout << "\nInserting 50 at position 10 (invalid):" << endl; list.insert(50, 10); return 0; } ``` ### 循环链表插入的核心原理与步骤详解 1. **空链表插入**: ```cpp if (!head) { head = newNode; head->next = head; // 节点自循环 } ``` - 第一个节点指向自己形成循环 - 不需要单独维护尾指针 2. **头部插入(position=0)**: ```cpp if (position == 0) { // 找到尾节点 while (tail->next != head) tail = tail->next; newNode->next = head; head = newNode; tail->next = head; // 更新尾节点的指针 } ``` - 必须更新尾节点的next指针 - 时间复杂度O(n),因需要定位尾节点 3. **中间/尾部插入**: ```cpp // 定位到插入位置的前一个节点 for (int i = 0; i < position - 1; i++) { current = current->next; } newNode->next = current->next; current->next = newNode; ``` - 与单向链表操作类似 - 自然维护循环特性(新节点指向后续节点) 4. **边界情况处理**: - 位置有效性检查(0 <= position <= size) - 空链表特殊处理 - 循环链表不会出现NULL指针问题 ### 循环链表的特性与注意事项 1. **循环特性维护**: - 每次插入必须维护头尾相接的特性 - 特别是头部插入需要更新尾节点指针 2. **大小维护**: - 使用size变量避免遍历计算长度 - size用于验证position的有效性 3. **内存管理**: - 析构函数需要特殊处理循环引用 - 使用do-while确保处理所有节点 4. **优势与应用**: - 适合需要循环访问的场景(播放列表、轮询系统) - 约瑟夫环问题的理想数据结构 - 比数组实现更灵活的内存使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值