एसटीडी से तत्वों को हटाना :: पुनरावृति करते समय सेट करें


147

मुझे एक सेट के माध्यम से जाने और पूर्वनिर्धारित मानदंडों को पूरा करने वाले तत्वों को हटाने की आवश्यकता है।

यह मेरे द्वारा लिखा गया परीक्षण कोड है:

#include <set>
#include <algorithm>

void printElement(int value) {
    std::cout << value << " ";
}

int main() {
    int initNum[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    std::set<int> numbers(initNum, initNum + 10);
    // print '0 1 2 3 4 5 6 7 8 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    std::set<int>::iterator it = numbers.begin();

    // iterate through the set and erase all even numbers
    for (; it != numbers.end(); ++it) {
        int n = *it;
        if (n % 2 == 0) {
            // wouldn't invalidate the iterator?
            numbers.erase(it);
        }
    }

    // print '1 3 5 7 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    return 0;
}

सबसे पहले, मैंने सोचा था कि सेट से एक तत्व को मिटाते समय इसके माध्यम से पुनरावृत्ति करना पुनरावृत्ति को अमान्य कर देगा, और लूप के लिए वेतन वृद्धि अपरिभाषित व्यवहार होगा। हालांकि, मैंने इस परीक्षण कोड को निष्पादित किया और सभी अच्छी तरह से चले गए, और मैं समझा नहीं सकता कि क्यों।

मेरा प्रश्न: क्या यह एसटीडी सेट के लिए परिभाषित व्यवहार है या यह कार्यान्वयन विशिष्ट है? मैं ubuntu 10.04 (32-बिट संस्करण) पर gcc 4.3.3 का उपयोग कर रहा हूं, वैसे।

धन्यवाद!

प्रस्तावित समाधान:

क्या यह सेट से तत्वों को पुनरावृत्त और मिटाने का एक सही तरीका है?

while(it != numbers.end()) {
    int n = *it;
    if (n % 2 == 0) {
        // post-increment operator returns a copy, then increment
        numbers.erase(it++);
    } else {
        // pre-increment operator increments, then return
        ++it;
    }
}

संपादित करें: पूर्वनिर्धारित समाधान

मैं एक समाधान के आसपास आया था जो मुझे अधिक सुरुचिपूर्ण लगता है, भले ही यह बिल्कुल वैसा ही हो।

while(it != numbers.end()) {
    // copy the current iterator then increment it
    std::set<int>::iterator current = it++;
    int n = *current;
    if (n % 2 == 0) {
        // don't invalidate iterator it, because it is already
        // pointing to the next element
        numbers.erase(current);
    }
}

यदि कुछ समय के भीतर कई परीक्षण स्थितियां हैं, तो उनमें से प्रत्येक को पुनरावृत्ति बढ़ाना चाहिए। मुझे यह कोड बेहतर पसंद है क्योंकि यह इटरेटर केवल एक ही स्थान पर बढ़ा हुआ है , जिससे कोड कम त्रुटि-प्रवण और अधिक पठनीय हो जाता है।


1
पूछा और उत्तर दिया: stackoverflow.com/questions/263945/…
मार्टिन यॉर्क

3
वास्तव में, मैंने यह सवाल (और अन्य) मेरा पूछने से पहले पढ़ा, लेकिन चूंकि वे अन्य एसटीएल कंटेनरों से संबंधित थे और चूंकि मेरे शुरुआती परीक्षण ने स्पष्ट रूप से काम किया था, मैंने सोचा कि उनके बीच कुछ अंतर था। मैट के जवाब के बाद ही मैंने वैल्गिंड का उपयोग करने के बारे में सोचा। हालांकि, मैं दूसरों पर अपना नया समाधान पसंद करता हूं क्योंकि यह केवल एक ही स्थान पर पुनरावृत्ति करने वाले को बढ़ाकर त्रुटियों की संभावना को कम करता है। मदद के लिए सभी का धन्यवाद!
18rom में पेड्रोमानोएल

1
@pedromanoel की ++itतुलना में कुछ अधिक कुशल होना चाहिए it++क्योंकि इसे पुनरावृत्त की एक अदृश्य अस्थायी प्रतिलिपि के उपयोग की आवश्यकता नहीं है। कॉर्नेल का संस्करण अब सुनिश्चित करता है कि गैर-फ़िल्टर किए गए तत्व सबसे अधिक कुशलता से पुन: प्रसारित होते हैं।
अलनीतक

@ अलनीतक मैंने उस बारे में नहीं सोचा है, लेकिन मुझे लगता है कि प्रदर्शन में अंतर इतना बढ़िया नहीं होगा। प्रतिलिपि उसके संस्करण में भी बनाई गई है, लेकिन केवल उन तत्वों के लिए जो मेल खाते हैं। इसलिए अनुकूलन की डिग्री पूरी तरह से सेट की संरचना पर निर्भर है। कुछ समय के लिए मैंने पूर्व-अनुकूलित कोड, इस प्रक्रिया में पठनीयता और कोडिंग गति को नुकसान पहुँचाया ... इसलिए मैं दूसरे तरीके का उपयोग करने से पहले कुछ परीक्षण करूंगा।
पेड्रोमानोएल

जवाबों:


178

यह कार्यान्वयन पर निर्भर है:

मानक 23.1.2.8:

सम्मिलित सदस्य पुनरावृत्तियों और कंटेनर के संदर्भों को प्रभावित नहीं करेंगे, और मिटाए गए सदस्य केवल पुनरावृत्तियों और अमान्य तत्वों के संदर्भों को अमान्य कर देंगे।

शायद आप यह कोशिश कर सकते हैं - यह मानक अनुरूप है:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        numbers.erase(it++);
    }
    else {
        ++it;
    }
}

ध्यान दें कि यह ++ पोस्टफ़िक्स है, इसलिए यह मिटाने के लिए पुरानी स्थिति से गुजरता है, लेकिन पहले ऑपरेटर के कारण एक नए से कूदता है।

2015.10.27 अपडेट: C ++ 11 ने दोष को हल कर दिया है। iterator erase (const_iterator position);अंतिम तत्व निकाले गए तत्व (या set::end, यदि अंतिम तत्व हटा दिया गया था) के लिए एक पुनरावर्तक लौटें । तो C ++ 11 शैली है:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        it = numbers.erase(it);
    }
    else {
        ++it;
    }
}

2
यह deque MSVC2013 पर काम नहीं करता है । या तो उनका कार्यान्वयन छोटी गाड़ी है या फिर एक और आवश्यकता है जो इस पर काम करने से रोकती है deque। एसटीएल कल्पना इतनी जटिल है कि आप इसे लागू करने के लिए सभी कार्यान्वयन की उम्मीद नहीं कर सकते हैं, अकेले अपने आकस्मिक प्रोग्रामर को इसे याद करने दें। एसटीएल एक राक्षस से परे है, और चूंकि कोई अनूठा कार्यान्वयन नहीं है (और परीक्षण सूट, यदि कोई हो, तो स्पष्ट रूप से लूप में तत्वों को हटाने जैसे स्पष्ट मामलों को कवर नहीं किया जाता है), जो एसटीएल को एक चमकदार भंगुर खिलौना बनाता है जो उसके साथ ऊपर जा सकता है एक बैंग जब आप इसे बग़ल में देखते हैं।
कुरो नीको

@MatthieuM। यह C ++ 11 में करता है। C ++ 17 में, अब itter (C_ 11 में const_iterator) लेता है।
tartaruga_casco_mole

19

यदि आप अपना प्रोग्राम वेलग्रिंड के माध्यम से चलाते हैं, तो आपको रीड एरर का एक गुच्छा दिखाई देगा। दूसरे शब्दों में, हां, पुनरावृत्तियों को अमान्य किया जा रहा है, लेकिन आप अपने उदाहरण में भाग्यशाली हो रहे हैं (या वास्तव में अशुभ, जैसा कि आप अपरिभाषित व्यवहार के नकारात्मक प्रभावों को नहीं देख रहे हैं)। इसका एक समाधान यह है कि एक अस्थायी पुनरावृत्ति बनाने के लिए, अस्थायी वृद्धि, लक्ष्य पुनरावृत्ति को हटाएं, फिर लक्ष्य को अस्थायी पर सेट करें। उदाहरण के लिए, अपने लूप को इस प्रकार फिर से लिखें:

std::set<int>::iterator it = numbers.begin();                               
std::set<int>::iterator tmp;                                                

// iterate through the set and erase all even numbers                       
for ( ; it != numbers.end(); )                                              
{                                                                           
    int n = *it;                                                            
    if (n % 2 == 0)                                                         
    {                                                                       
        tmp = it;                                                           
        ++tmp;                                                              
        numbers.erase(it);                                                  
        it = tmp;                                                           
    }                                                                       
    else                                                                    
    {                                                                       
        ++it;                                                               
    }                                                                       
} 

अगर यह केवल ऐसी स्थिति है जो मायने रखती है और इन-स्कोप इनिशियलाइज़ेशन या पोस्ट-ऑपरेशन की आवश्यकता नहीं है, तो whileलूप का उपयोग करना बेहतर है । यानी for ( ; it != numbers.end(); )बेहतर हैwhile (it != numbers.end())
iammilind

7

आप गलत समझते हैं कि "अपरिभाषित व्यवहार" का क्या अर्थ है। अपरिभाषित व्यवहार का मतलब यह नहीं है "यदि आप ऐसा करते हैं, तो आपका कार्यक्रम अप्रत्याशित परिणाम देगा या अप्रत्याशित परिणाम देगा।" इसका अर्थ है "यदि आप ऐसा करते हैं, तो आपका कार्यक्रम अप्रत्याशित परिणाम प्राप्त कर सकता है या अनपेक्षित परिणाम उत्पन्न कर सकता है ", या आपके कंपाइलर, आपके ऑपरेटिंग सिस्टम, चंद्रमा के चरण आदि के आधार पर और कुछ भी कर सकता है।

यदि कोई दुर्घटनाग्रस्त हो जाता है और व्यवहार करता है जैसा कि आप इसकी अपेक्षा करते हैं, तो यह प्रमाण नहीं है कि यह अपरिभाषित व्यवहार नहीं है। यह सब साबित करता है कि इसका व्यवहार उस विशेष संकलक के साथ उस विशेष संकलक प्रणाली के संकलन के बाद उस विशेष रन के लिए देखा गया था।

एक सेट से एक तत्व को मिटाकर मिटाने वाले तत्व को पुनरावृत्ति को अमान्य करता है। अमान्य पुनरावृत्ति का उपयोग करना अपरिभाषित व्यवहार है। यह सिर्फ इतना हुआ कि मनाया गया व्यवहार वह था जो आप इस विशेष उदाहरण में करना चाहते थे; इसका मतलब यह नहीं है कि कोड सही है।


ओह, मैं अच्छी तरह से जानता हूं कि अपरिभाषित व्यवहार का मतलब यह भी हो सकता है कि "यह मेरे लिए काम करता है, लेकिन हर किसी के लिए नहीं"। इसलिए मैंने यह सवाल पूछा, क्योंकि मुझे नहीं पता था कि यह व्यवहार सही था या नहीं। अगर ऐसा होता, तो मैं बस ऐसे ही छोड़ देता। थोड़ी देर लूप का उपयोग करने से मेरी समस्या हल हो जाएगी? मैंने अपने प्रस्तावित समाधान के साथ अपना प्रश्न संपादित किया। कृपया यह देखें।
पेड्रोमानोएल

यह मेरे लिए भी काम करता है। लेकिन जब मैं स्थिति को बदल देता if (n > 2 && n < 7 )हूं तो मुझे 0 1 2 4 7 8 9. प्राप्त होता है - विशेष रूप से यहां का परिणाम संभवतः मिटा विधि के कार्यान्वयन के विवरण पर निर्भर करता है और पुनरावृत्तियों को सेट करता है, बजाय चंद्रमा के चरण पर (एक भी नहीं कभी कार्यान्वयन विवरण पर भरोसा करना चाहिए)। ;)
चाचा

1
एसटीएल "अपरिभाषित व्यवहार" के लिए बहुत सारे नए अर्थ जोड़ता है। उदाहरण के लिए "Microsoft ने std::set::eraseएक आइटर को वापस करने की अनुमति देकर युक्ति को बढ़ाने के लिए स्मार्ट सोचा , इसलिए आपका MSVC कोड gcc द्वारा संकलित किए जाने पर एक धमाके के साथ ऊपर जाएगा", या "Microsoft चेक को बाध्य करता है std::bitset::operator[]ताकि आपके सावधानी से अनुकूलित बिटसेट एल्गोरिथ्म धीमा हो जाए क्रॉल जब MSVC के साथ संकलित किया जाता है ”। एसटीएल का कोई अनोखा कार्यान्वयन नहीं है और इसकी कल्पना एक तेजी से विकसित फूला हुआ गड़बड़ है, इसलिए कोई आश्चर्य नहीं कि एक लूप के अंदर से तत्वों को हटाने के लिए वरिष्ठ प्रोग्रामर विशेषज्ञता की आवश्यकता होती है ...
kuroi neko

2

बस चेतावनी देने के लिए, कि एक डॉक कंटेनर के मामले में, सभी समाधान जो संख्याओं के लिए ईक ईटर की समानता की जांच करते हैं। () संभवतः gcc 4.8.4 पर विफल होंगे। आमतौर पर, एक तत्व को मिटाना आमतौर पर सूचक को नंबरों को अमान्य कर देता है। ():

#include <iostream>
#include <deque>

using namespace std;
int main() 
{

  deque<int> numbers;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  //numbers.push_back(4);

  deque<int>::iterator  it_end = numbers.end();

  for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
      cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      if (it_end == numbers.end()) {
    cout << "it_end is still pointing to numbers.end()\n";
      } else {
    cout << "it_end is not anymore pointing to numbers.end()\n";
      }
    }
    else {
      cout << "Skipping element: " << *it << "\n";
      ++it;
    }
  }
}

आउटपुट:

Erasing element: 0
it_end is still pointing to numbers.end()
Skipping element: 1
Erasing element: 2
it_end is not anymore pointing to numbers.end()

ध्यान दें कि जबकि इस विशेष मामले में deque परिवर्तन सही है, अंत सूचक को रास्ते में अमान्य कर दिया गया है। एक अलग आकार के छल के साथ त्रुटि अधिक स्पष्ट है:

int main() 
{

  deque<int> numbers;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  numbers.push_back(4);

  deque<int>::iterator  it_end = numbers.end();

  for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
      cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      if (it_end == numbers.end()) {
    cout << "it_end is still pointing to numbers.end()\n";
      } else {
    cout << "it_end is not anymore pointing to numbers.end()\n";
      }
    }
    else {
      cout << "Skipping element: " << *it << "\n";
      ++it;
    }
  }
}

आउटपुट:

Erasing element: 0
it_end is still pointing to numbers.end()
Skipping element: 1
Erasing element: 2
it_end is still pointing to numbers.end()
Skipping element: 3
Erasing element: 4
it_end is not anymore pointing to numbers.end()
Erasing element: 0
it_end is not anymore pointing to numbers.end()
Erasing element: 0
it_end is not anymore pointing to numbers.end()
...
Segmentation fault (core dumped)

इसे ठीक करने के तरीकों में से एक है:

#include <iostream>
#include <deque>

using namespace std;
int main() 
{

  deque<int> numbers;
  bool done_iterating = false;

  numbers.push_back(0);
  numbers.push_back(1);
  numbers.push_back(2);
  numbers.push_back(3);
  numbers.push_back(4);

  if (!numbers.empty()) {
    deque<int>::iterator it = numbers.begin();
    while (!done_iterating) {
      if (it + 1 == numbers.end()) {
    done_iterating = true;
      } 
      if (*it % 2 == 0) {
    cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      }
      else {
    cout << "Skipping element: " << *it << "\n";
    ++it;
      }
    }
  }
}

की जा रही है do not trust an old remembered dq.end() value, always compare to a new call to dq.end()
जेसी चिशोल्म

2

C ++ 20 में "यूनिफ़ॉर्म कंटेनर इरेज़र" होगा, और आप लिख पाएंगे:

std::erase_if(numbers, [](int n){ return n % 2 == 0 });

और उस के लिए काम करेंगे vector, set, deque, आदि देखें cppReference अधिक जानकारी के लिए।


1

यह व्यवहार विशिष्ट कार्यान्वयन है। पुनरावृत्ति की शुद्धता की गारंटी के लिए आपको "it = numbers.erase (it);" का उपयोग करना चाहिए यदि आप तत्व को हटाने की जरूरत है और अन्य मामले में बस पुनरावृत्ति itter बयान।


1
Set<T>::eraseसंस्करण पुनरावृत्ति वापस नहीं करता है।
अरकिट्ज जिमेनेज

4
वास्तव में यह करता है, लेकिन केवल MSVC कार्यान्वयन पर। तो यह वास्तव में एक कार्यान्वयन विशिष्ट उत्तर है। :)
यूजीन

1
@ यूजीन इसे C ++ 11 के साथ सभी कार्यान्वयनों के लिए करता है
mastov

कुछ कार्यान्वयन के gcc 4.8साथ c++1yएक बग को मिटा दिया गया है। it = collection.erase(it);काम करना माना जाता है, लेकिन यह उपयोग करने के लिए सुरक्षित हो सकता हैcollection.erase(it++);
जेसी चिशोल्म

1

मुझे लगता है कि एसटीएल विधि का उपयोग करने remove_ifसे 'कुछ अजीब मुद्दे को रोकने में मदद मिल सकती है जब पुनरावृत्ति करने वाले ऑब्जेक्ट को हटाने की कोशिश कर रहा है।

यह समाधान कम कुशल हो सकता है।

मान लें कि हमारे पास कुछ प्रकार के कंटेनर हैं, जैसे वेक्टर या एक सूची जिसे m_bullets कहा जाता है:

Bullet::Ptr is a shared_pr<Bullet>

' it' इट्रेटर है कि ' remove_if' रिटर्न, तीसरा तर्क एक लंबो फ़ंक्शन है जो कंटेनर के प्रत्येक तत्व पर निष्पादित होता है। क्योंकि कंटेनर में Bullet::Ptrलैंबडा फ़ंक्शन को उस प्रकार (या उस प्रकार का एक संदर्भ) को तर्क के रूप में पारित करने की आवश्यकता होती है।

 auto it = std::remove_if(m_bullets.begin(), m_bullets.end(), [](Bullet::Ptr bullet){
    // dead bullets need to be removed from the container
    if (!bullet->isAlive()) {
        // lambda function returns true, thus this element is 'removed'
        return true;
    }
    else{
        // in the other case, that the bullet is still alive and we can do
        // stuff with it, like rendering and what not.
        bullet->render(); // while checking, we do render work at the same time
        // then we could either do another check or directly say that we don't
        // want the bullet to be removed.
        return false;
    }
});
// The interesting part is, that all of those objects were not really
// completely removed, as the space of the deleted objects does still 
// exist and needs to be removed if you do not want to manually fill it later 
// on with any other objects.
// erase dead bullets
m_bullets.erase(it, m_bullets.end());

' remove_if' उस कंटेनर को हटा देता है जहाँ लंबोदर फ़ंक्शन सही लौटा है और उस सामग्री को कंटेनर की शुरुआत में स्थानांतरित करता है। ' it' एक अपरिभाषित वस्तु की ओर इशारा करता है जिसे कचरा माना जा सकता है। ऑब्जेक्ट्स 'इट' से m_bullets.end () तक मिटाए जा सकते हैं, क्योंकि वे मेमोरी पर कब्जा कर लेते हैं, लेकिन इसमें कचरा होता है, इस प्रकार उस रेंज पर 'इरेज़' विधि कहा जाता है।


0

मैं एक ही पुराने मुद्दे पर आया था और नीचे दिए गए कोड को अधिक समझा जा सकता है जो कि उपरोक्त समाधानों के अनुसार है।

std::set<int*>::iterator beginIt = listOfInts.begin();
while(beginIt != listOfInts.end())
{
    // Use your member
    std::cout<<(*beginIt)<<std::endl;

    // delete the object
    delete (*beginIt);

    // erase item from vector
    listOfInts.erase(beginIt );

    // re-calculate the begin
    beginIt = listOfInts.begin();
}

यह केवल तभी काम करेगा जब आप हमेशा हर वस्तु को मिटा देंगे । ओपी चुनिंदा वस्तुओं को मिटा देने और अभी भी वैध पुनरावृत्तियों के बारे में है।
जेसी चिशोल्म
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.