วนลูป แล้วลบ “ของ” ออกจาก list

คุณเคยต้องเขียนโปรแกรมที่วนลูปบน list แล้วลบ “ของ (object)” ออกจาก list นั้นหรือไม่? ลองยกตัวอย่างโปรแกรมง่ายๆ สมมติให้ list A เก็บเลขจำนวนเต็ม แล้วคุณต้องการลบเลขที่หารสามลงตัว ออกจาก list A โค้ดของคุณก็อาจจะเป็น

Python C#
A = [1, 2, 3, 6, 7, 9, 12]
for x in A:
    if x % 3 == 0:
        A.remove(x)

List A = new List(new int[]{1, 2, 3, 6, 7, 9, 12});
foreach(int x in A)
{
    if(x % 3 == 0)
        A.Remove(x);
}

ในบล็อกนี้ขออนุญาตยกตัวอย่างแค่ 2 ภาษา คือ Python และ C# นะครับ สำหรับฝั่ง C# จะเจอกับ “Unhandled Exception: System.InvalidOperationException: Collection was modified;” (เขาบอกว่า เกิดการกระทำที่ไม่ถูกต้อง เนื่องจาก list ถูกแก้ไขระหว่างวนลูป) ส่วนคนใช้ Python จะไม่เจอปัญหาอะไรเวลารันครับ แต่พอลองพิมพ์ A ออกมาดูจะพบว่า

A = [1, 2, 6, 7, 12]

แงว… ทำไม 6 กับ 12 ยังโผล่ออกมา? ในเมื่อมันน่าจะ mod 3 ลงตัว… ในบล็อกนี้เราจะมาดูวิธีแก้ปัญหาที่มักจะเจอบ่อยๆ ปัญหานี้กันครับ

ปัญหาที่เจอใน Python คือ เลขจำนวนเต็มที่หารสามลงตัว ไม่โดนลบออกไปทุกตัว สาเหตุคือ ในการวนลูปบน list python จะมีตัวชี้ index ของ list สร้างขึ้นมา และในการวนลูปแต่ละรอบ index จะต้องโดนขยับไป 1 เสมอ ดังนั้นเมื่อตัวเลขโดนลบไป ตัวเลขอื่นๆ ที่อยู่ต่อท้ายจะเลื่อนมาแทนที่ตัวที่หายไป แล้วตัวที่เลื่อนมาแทนที่เนี่ยแหละครับ จะโดนข้ามไป (ไม่โดนตรวจสอบ)…. ถ้างง ลองดูตัวอย่างต่อไปนี้ครับ

A = 1 2 3 6 7 9 12
พิจารณา A[0] คือ 1 หารสาม ไม่ลงตัว
A = 1 2 3 6 7 9 12
พิจารณา A[1] คือ 2 หารสาม ไม่ลงตัว
A = 1 2 3 6 7 9 12
พิจารณา A[2] คือ 3 หารสามลงตัว ลบทิ้ง!
A = 1 2 6 7 9 12
list A กลายเป็นแบบนี้ จะเห็นว่า 6 เลื่อนมาอยู่ที่ A[2] แต่ python ก็ยังเลื่อน index ไปหนึ่งช่อง ไปพิจารณา A[3] เลย ทำให้เลข 6 โดนข้ามไป

จะเห็นว่ามีกรณีแบบนี้เกิดขึ้นได้ใน python และคนเขียนโปรแกรมก็ไม่รู้ว่าเกิดปัญหาขึ้นเลย (นอกจากมาดูผลลัพธ์) ดังนั้นใน C# เขาจึงห้ามวนลูปแบบ foreach แล้วแก้ไขข้อมูลใน list (เกิด InvalidOperationException นั่นเอง) ในเมื่อ foreach ใช้ไม่ได้ ขอลักไก่ใช้ for ธรรมดาแทนได้มั้ย? คำตอบคือ ไม่ได้ครับ ถ้าลองเขียนแล้วจะเจอปัญหาเหมือนกับที่เจอใน Python ครับ

แล้วเราจะแก้ปัญหานี้ยังไงดีหละ?

แบ่งได้เป็น 3 วิธีที่เจอบ่อยๆ ครับ

1. สร้าง List อีกอันมาเก็บเฉพาะผลลัพธ์ที่ต้องการ

วิธีนี้ง่ายๆ ตรงๆ เลยครับ ในเมื่อมี list A แล้วเราต้องการเฉพาะเลขที่หารสามไม่ลงตัว แล้วใช้ list ใหม่เป็นคำตอบ เราก็เขียนโปรแกรมประมาณนี้ได้เลยครับ

Python C#
B = []
for x in A:
    if x % 3  0:
        B.append(x)

List B = new List();
foreach(int x in A)
{
    if (x % 3 != 0)
	B.Add(x);
}

อ่าวๆๆแบบนี้ โปรแกรมก็ต้อง copy ตัวเลขทุกตัว ไปใส่ list B แทน ซึ่งมันต้องทำงานช้าแน่ๆ เลย สมมติว่าเราต้องการลบเลขแค่ตัวเดียวใน list A แต่ดันต้อง copy ตัวที่ต้องการเก็บไว้ไปใส่ใน list B ซึ่งเสียเวลาทำงานตั้งนาน (แต่! จะเร็วกว่าวิธีอื่นๆ ถ้ามีตัวเลขที่ต้องโดนลบเยอะๆ ครับ)

งั้นไปดูวิธีที่สองดีกว่า

2. วนลูปในตัวก๊อบปี้ของ List

วิธีนี้คือ สร้างตัวก๊อบปี้ของ List (เอาตัวเลขทุกตัว) มาเลยครับ เอามาใช้วนลูป แล้วเวลาลบข้อมูลก็ลบใน list ของจริง ผลลัพธ์ก็จะเก็บอยู่ใน list A ตามปกติ

Python C#
for x in A[:]:
    if x % 3 == 0:
        A.remove(x)

foreach(int x in new List(A))
{
    if (x % 3 == 0)
	A.Remove(x);
}
อธิบายเพิ่มเติม:A[:] คือการสร้างตัวก๊อบปี้ของ list A ครับ อธิบายเพิ่มเติม:new List(A) คือการสร้างตัวก๊อบปี้ของ list A ครับ

หลายๆ คนคงจะเห็นว่า มันก็คล้ายๆ กับวิธีที่ 1 แหละน๊า… แค่สร้างตัวก๊อบปี้ไว้วนลูป ต้องเสียเวลาสร้างตัวก๊อบปี้เหมือนกันไม่ใช่เรอะ?… ฮึ่ม! งั้นลองไปดูวิธีที่ 3 กันครับ

3. วนลูปถอยหลัง

ในเมื่อวนลูปจากหน้าไปหลัง (0 ไปจนถึงขนาดของ list) จะมีปัญหาเพราะว่า ตัวเลขใน list จะเลื่อนมาแทนที่ตัวที่ถูกลบ ดังนั้นเราแก้ปัญหาโดยวนลูปถอยหลัง แทนครับ นั่นก็คือ วนลูปจากขนาดของ list มาจนถึง 0 นั่นเอง

Python C#
for i in reversed(range(len(A)))
    if x % 3 == 0:
        del A[i]
for(int i = A.Count - 1; i >= 0; i--)
{
    if (A[i] % 3 == 0)
	A.RemoveAt(i);
}

จากตัวอย่างโค้ดจะเห็นว่าเราใช้ del A[i] และ A.RemoveAt(i) เพื่อลบตัวเลข ที่ตำแหน่งใดๆ แทนที่จะใช้ Remove ที่ใช้ลบค่าใดๆ จะเห็นว่าวิธีที่สามนั้นน่าจะทำงานได้เร็วกว่าวิธีที่ 1 และ 2 เพราะไม่ต้องสร้างตัวก๊อบปี้ของ list นั่นเอง

สรุป

จากบล็อกนี้เราได้เรียนรู้วิธีแก้ปัญหาที่เกิดขึ้นจากการวนลูป แล้วลบ “ของ” ออกจาก list โดยแบ่งออกเป็น 3 วิธีที่เจอบ่อยๆ ซึ่งแต่ละวิธีก็มีประสิทธิภาพแตกต่างกันไปขึ้นกับการนำไปใช้ และความแตกต่างของแต่ละภาษา ใครชอบใครถนัดวิธีไหนก็เลือกไปใช้ตามสะดวก😉

ส่วนตั๊วส่วนตัวจากคนเขียน

ปล. สาเหตุที่มานั่งเขียน blog นี้เพราะผมเขียนโปรแกรมไปแล้วก็เจอ bug… คือ “ของ” ใน list มันโดนลบออกไปไม่หมด (แต่ python ไม่ยอมเตือนเหมือน C# หง่ะ!!) ก็เลยนั่งหา bug จนมาเจอ… เอาเป็นว่า เพื่อนๆ คนอื่นที่อ่านมาเจอบล็อกนี้แล้ว คงไม่พลาดแบบผมละกันครับ

ปล2. เพิ่งรู้ว่าใช้ reversed() แล้วโค้ดสวยกว่าเดิมเยอะเลย ปกติเวลาต้องการวนลูปจากมากไปน้อย (9 ถึง 0) ก็เขียนโค้ดน่าเกลียดๆ แบบนี้

for i in range(9, -1, -1):
    print i

พอเขียนบล็อกนี้แล้วเพิ่งรู้ว่าเขียนเป็น

for i in reversed(range(0, 9)):
    print i

สวยกว่าเยอะเลย😀

ปล3. ใครอยากให้เขียนบล็อก แล้วยกตัวอย่างภาษาอื่นๆ นอกจาก Python และ C# ช่วยทิ้งคอมเม็นท์ไว้ด้วยครับ

6 คิดบน “วนลูป แล้วลบ “ของ” ออกจาก list

  1. ขอบใจมากเลย มีประโยชน์จริงๆ
    เราเพิ่งเริ่มฝึก python นิดเดียว
    อ่านอันนี้ได้ประโยชน์จริงๆ เพิ่งสังเกตเหมือนกัน

    1. N = [[1,1,2],23,3,[2,3]]
      N2 = []
      for x in N:
          if type(x) == list:
              N2 += x
          else:
              N2.append(x)
      

      หรือเขียนแบบ list comprehension ได้เป็น

      N = [[1,1,2],23,3,[2,3]]
      N2=sum((x if type(x) is list else [x] for x in N), [])
      

      ถ้ามันเป็น list ของ list แล้วอยากจะ flat เช่น
      N = [[1,1,2], [23], [3], [2, 3]]
      ให้ลองอ่านคำถามใน stackoverflow อันนี้ครับ มีท่าแปลกๆ เพียบเลย

ใส่ความเห็น

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / เปลี่ยนแปลง )

Twitter picture

You are commenting using your Twitter account. Log Out / เปลี่ยนแปลง )

Facebook photo

You are commenting using your Facebook account. Log Out / เปลี่ยนแปลง )

Google+ photo

You are commenting using your Google+ account. Log Out / เปลี่ยนแปลง )

Connecting to %s