มีอัลกอริธึมการค้นหาที่ดีสำหรับอักขระเดี่ยวหรือไม่?


23

ฉันรู้อัลกอริธึมการจับคู่สตริงพื้นฐานหลายอย่างเช่น KMP หรือ Boyer-Moore แต่สิ่งเหล่านี้วิเคราะห์รูปแบบก่อนค้นหาอย่างไรก็ตามถ้ามีตัวอักษรตัวเดียวก็ไม่ได้วิเคราะห์อะไรมากมาย ดังนั้นมีอัลกอริทึมที่ดีกว่าการค้นหาที่ไร้เดียงสาของการเปรียบเทียบอักขระทุกตัวของข้อความหรือไม่


13
คุณสามารถโยนคำแนะนำ SIMD ได้ แต่คุณจะไม่ได้อะไรที่ดีไปกว่า O (n)
CodesInChaos

7
สำหรับการค้นหาเดียวหรือการค้นหาหลายรายการในสตริงเดียวกัน
Christophe

KMP ไม่ใช่สิ่งที่ฉันจะเรียกอัลกอริทึมการจับคู่สตริง "พื้นฐาน" แน่นอนฉันไม่แน่ใจด้วยซ้ำว่ามันเร็วเช่นนี้ แต่มันมีความสำคัญในอดีต หากคุณต้องการบางสิ่งบางอย่างขั้นพื้นฐานลองใช้อัลกอริทึม Z
Mehrdad

สมมติว่ามีตำแหน่งตัวอักษรอัลกอริทึมการค้นหาไม่ได้ดู จากนั้นมันจะไม่สามารถแยกความแตกต่างระหว่างสตริงกับอักขระเข็มในตำแหน่งนั้นและสตริงที่มีอักขระอื่นในตำแหน่งนั้น
user253751

คำตอบ:


29

เป็นที่เข้าใจกันว่ากรณีที่เลวร้ายที่สุดคือO(N)มีการเพิ่มประสิทธิภาพขนาดเล็กมาก

วิธีการที่ไร้เดียงสาทำการเปรียบเทียบอักขระและการเปรียบเทียบสิ้นสุดข้อความสำหรับแต่ละอักขระ

การใช้Sentinel (เช่นสำเนาของตัวละครเป้าหมายที่ส่วนท้ายของข้อความ) ลดจำนวนการเปรียบเทียบ 1 ต่อตัวละคร

ที่ระดับ twiddling มีอยู่:

#define haszero(v)      ( ((v) - 0x01010101UL) & ~(v) & 0x80808080UL )
#define hasvalue(x, n)  ( haszero((x) ^ (~0UL / 255 * (n))) )

เพื่อทราบว่าไบต์ใด ๆ ในคำ ( x) มีค่าเฉพาะ ( n)

subexpression v - 0x01010101ULประเมินชุดบิตสูงในไบต์เมื่อใดก็ตามไบต์สอดคล้องกันในการเป็นศูนย์หรือมากกว่าv0x80

นิพจน์ย่อย~v & 0x80808080ULประเมินค่าเป็นบิตสูงที่ตั้งเป็นไบต์ที่ไบต์ของvไม่มีชุดบิตสูง (ดังนั้นไบต์น้อยกว่า0x80)

โดย ANDing นิพจน์ย่อยทั้งสองนี้ ( haszero) ผลลัพธ์คือชุดบิตสูงที่ไบต์vเป็นศูนย์ตั้งแต่บิตสูงตั้งเนื่องจากค่าที่มากกว่า0x80ในนิพจน์ย่อยแรกถูกปิดโดยวินาที (27 เมษายน) 2530 โดยอลัน Mycroft)

ตอนนี้เราสามารถ XOR ค่าที่จะทดสอบ ( x) ด้วยคำที่เต็มไปด้วยค่าไบต์ที่เราสนใจ ( n) เพราะ XORing haszeroค่ากับตัวเองส่งผลให้ไบต์และไม่ใช่ศูนย์ศูนย์มิฉะนั้นเราสามารถส่งผลให้

สิ่งนี้มักใช้ในstrchrการนำไปใช้โดยทั่วไป

(Stephen M Bennet แนะนำสิ่งนี้ในวันที่ 13 ธันวาคม 2009 รายละเอียดเพิ่มเติมในBit Twiddling Hacks ที่รู้จักกันดี)


PS

รหัสนี้เสียสำหรับการรวมกันของ1111ถัดจาก0

การแฮกผ่านการทดสอบกำลังดุร้าย (แค่อดทน):

#include <iostream>
#include <limits>

bool haszero(std::uint32_t v)
{
  return (v - std::uint32_t(0x01010101)) & ~v & std::uint32_t(0x80808080);
}

bool hasvalue(std::uint32_t x, unsigned char n)
{
  return haszero(x ^ (~std::uint32_t(0) / 255 * n));
}

bool hasvalue_slow(std::uint32_t x, unsigned char n)
{
  for (unsigned i(0); i < 32; i += 8)
    if (((x >> i) & 0xFF) == n)
      return true;

  return false;
}

int main()
{
  const std::uint64_t stop(std::numeric_limits<std::uint32_t>::max());

  for (unsigned c(0); c < 256; ++c)
  {
    std::cout << "Testing " << c << std::endl;

    for (std::uint64_t w(0); w != stop; ++w)
    {
      if (w && w % 100000000 == 0)
        std::cout << w * 100 / stop << "%\r" << std::flush;

      const bool h(hasvalue(w, c));
      const bool hs(hasvalue_slow(w, c));

      if (h != hs)
        std::cerr << "hasvalue(" << w << ',' << c << ") is " << h << '\n';
    }
  }

  return 0;
}

จำนวน upvotes สำหรับคำตอบซึ่งทำให้สมมติฐานหนึ่งอักขระ = หนึ่งไบต์ซึ่งทุกวันนี้ไม่ใช่มาตรฐานอีกต่อไป

ขอบคุณสำหรับข้อสังเกต

คำตอบนั้นหมายถึงอะไร แต่เป็นบทความเกี่ยวกับการเข้ารหัสแบบหลายไบต์ / ตัวแปรความกว้าง :-) (ในความเป็นธรรมทั้งหมดที่ไม่ใช่ความเชี่ยวชาญของฉันและฉันไม่แน่ใจว่าเป็นสิ่งที่ OP กำลังมองหา)

อย่างไรก็ตามสำหรับฉันแล้วความคิด / เทคนิคข้างต้นสามารถปรับให้เข้ากับ MBE ได้บ้าง (โดยเฉพาะการเข้ารหัสการซิงโครไนซ์ด้วยตนเอง ):

  • ตามที่ระบุไว้ในความคิดเห็นของ Johanการแฮ็คสามารถขยายได้อย่างง่ายดายเพื่อทำงานสองไบต์หรืออะไรก็ได้ (แน่นอนว่าคุณไม่สามารถยืดได้มากเกินไป)
  • ฟังก์ชั่นทั่วไปที่หาตำแหน่งของตัวละครในสตริงอักขระหลายไบต์:
    • มีการเรียกไปยังstrchr/ strstr(เช่นGNUlib coreutils mbschr )
    • คาดว่าพวกเขาจะได้รับการปรับอย่างดี
  • เทคนิคยามสามารถใช้กับการมองการณ์ไกลเล็กน้อย

1
นี่เป็นรุ่นที่ใช้งานร่วมกับ SIMD ของคนยากจน
Ruslan

@ Ruslan แน่นอน! นี่เป็นกรณีของการแฮ็กบิตที่มีประสิทธิภาพ
manlio

2
คำตอบที่ดี จากแง่มุมที่อ่านได้ฉันไม่เข้าใจว่าทำไมคุณเขียน0x01010101ULในหนึ่งบรรทัดและ~0UL / 255ในถัดไป มันให้ความรู้สึกว่าพวกเขาจะต้องมีค่าที่แตกต่างกันมิฉะนั้นแล้วทำไมต้องเขียนมันด้วยสองวิธีที่ต่างกัน?
hvd

3
นี้จะเย็นเพราะมันจะตรวจสอบ 4 ไบต์ในครั้งเดียว แต่มันต้องใช้หลาย ๆ (8) คำแนะนำตั้งแต่#defines ( (((x) ^ (0x01010101UL * (n)))) - 0x01010101UL) & ~((x) ^ (0x01010101UL * (n)))) & 0x80808080UL )จะขยายตัวออกไป การเปรียบเทียบไบต์เดียวจะไม่เร็วกว่านี้หรือ
Jed Schaaf

1
@DocBrown รหัสสามารถทำให้ทำงานได้อย่างง่ายดายสำหรับไบต์คู่ (เช่น halfwords) หรือ nibbles หรืออะไรก็ได้ (คำนึงถึงข้อแม้ที่ฉันกล่าวถึง)
Johan - คืนสถานะโมนิก้า

20

อัลกอริธึมการค้นหาข้อความใด ๆ ที่ค้นหาการเกิดขึ้นของอักขระเดี่ยวในข้อความที่กำหนดจะต้องอ่านอักขระแต่ละตัวของข้อความอย่างน้อยหนึ่งครั้งซึ่งควรชัดเจน และเนื่องจากนี่เพียงพอสำหรับการค้นหาครั้งเดียวจึงไม่มีอัลกอริธึมที่ดีกว่านี้ (เมื่อคิดในแง่ของลำดับเวลารันซึ่งเรียกว่า "linear" หรือ O (N) สำหรับกรณีนี้โดยที่ N คือจำนวนตัวอักษร เพื่อค้นหา)

อย่างไรก็ตามสำหรับการใช้งานจริงนั้นมีการปรับให้เหมาะสมจำนวนมากที่เป็นไปได้ซึ่งไม่เปลี่ยนลำดับเวลาทำงานโดยรวม แต่ลดเวลาทำงานจริง และหากเป้าหมายไม่ใช่การค้นหาทุกครั้งของตัวละครเดียว แต่มีเพียงตัวแรกเท่านั้นคุณสามารถหยุดได้ตั้งแต่เกิดครั้งแรกแน่นอน อย่างไรก็ตามแม้ในกรณีนั้นกรณีที่เลวร้ายที่สุดยังคงเป็นตัวละครที่คุณกำลังมองหาเป็นตัวละครสุดท้ายในข้อความดังนั้นคำสั่งรันไทม์ของกรณีที่แย่ที่สุดสำหรับเป้าหมายนี้ยังคงเป็น O (N)


8

หาก "กองหญ้า" ของคุณถูกค้นหามากกว่าหนึ่งครั้งวิธีฮิสโตแกรมที่ใช้จะเร็วมาก หลังจากสร้างฮิสโตแกรมแล้วคุณจะต้องค้นหาตัวชี้เพื่อค้นหาคำตอบของคุณ

หากคุณต้องการทราบว่ารูปแบบการค้นหานั้นมีอยู่หรือไม่ตัวนับแบบง่ายสามารถช่วยได้ สามารถขยายได้เพื่อรวมตำแหน่งที่พบอักขระแต่ละตัวในกองหญ้าหรือตำแหน่งที่เกิดขึ้นครั้งแรก

string haystack = "agtuhvrth";
array<int, 256> histogram{0};
for(character: haystack)
     ++histogram[character];

if(histogram['a'])
    // a belongs to haystack

1

หากคุณต้องการค้นหาอักขระในสตริงเดียวกันนี้มากกว่าหนึ่งครั้งวิธีที่เป็นไปได้คือการแบ่งสตริงออกเป็นส่วนเล็ก ๆ ซ้ำ ๆ ซ้ำ ๆ และใช้ฟิลเตอร์บลูมสำหรับแต่ละส่วนเหล่านี้

เนื่องจากฟิลเตอร์บลูมสามารถบอกคุณได้อย่างแน่นอนว่าอักขระไม่ได้อยู่ในส่วนของสตริงที่ฟิลเตอร์ "แสดง" คุณสามารถข้ามบางส่วนขณะค้นหาอักขระ

ตัวอย่างเช่น: สำหรับสตริงต่อไปนี้หนึ่งสามารถแยกออกเป็น 4 ส่วน (แต่ละตัวยาว 11 ตัวอักษร) และเติมแต่ละส่วนเป็นตัวกรองบลูม (อาจมีขนาดใหญ่ 4 ไบต์) ด้วยอักขระของส่วนนั้น:

The quick brown fox jumps over the lazy dog 
          |          |          |          |

คุณสามารถเพิ่มความเร็วในการค้นหาของคุณเช่นสำหรับตัวละครa: การใช้ฟังก์ชั่นแฮชที่ดีสำหรับฟิลเตอร์บลูมพวกเขาจะบอกคุณว่า - ด้วยความน่าจะเป็นสูง - คุณไม่ต้องค้นหาทั้งในส่วนที่หนึ่งสองหรือสาม ดังนั้นคุณสามารถป้องกันตัวเองจากการตรวจสอบ 33 ตัวอักษรและแทนที่จะต้องตรวจสอบ 16 ไบต์ (สำหรับตัวกรอง 4 ดอก) สิ่งนี้ยังคงO(n)มีเพียงปัจจัยคงที่ (เศษส่วน) (และเพื่อให้สิ่งนี้มีประสิทธิภาพคุณจะต้องเลือกชิ้นส่วนที่ใหญ่กว่าเพื่อลดค่าใช้จ่ายในการคำนวณฟังก์ชันแฮชสำหรับอักขระการค้นหา)

การใช้วิธีเรียกซ้ำแบบต้นไม้จะช่วยให้คุณเข้าใกล้O(log n):

The quick brown fox jumps over the lazy dog 
   |   |   |   |   |   |   |   |---|-X-|   |  (1 Byte)
       |       |       |       |---X---|----  (2 Byte)
               |               |-----X------  (3 Byte)
-------------------------------|-----X------  (4 Byte)
---------------------X---------------------|  (5 Byte)

ในการกำหนดค่านี้เราต้องการ (อีกครั้งสมมติว่าเราโชคดีและไม่ได้รับผลบวกผิด ๆ จากตัวกรองตัวใดตัวหนึ่ง) เพื่อตรวจสอบ

5 + 2*4 + 3 + 2*2 + 2*1 bytes

เพื่อไปยังส่วนสุดท้าย (ที่หนึ่งต้องตรวจสอบ 3 ตัวอักษรจนกว่าจะหาa)

การใช้รูปแบบการจัดสรรที่ดี (ดีกว่าตามที่กล่าวไว้) คุณควรได้ผลลัพธ์ที่ดีงาม (หมายเหตุ: ตัวกรองบลูมที่รากของต้นไม้ควรมีขนาดใหญ่กว่าใกล้กับใบดังแสดงในตัวอย่างเพื่อให้ได้ค่าความน่าจะเป็นบวกที่ต่ำ)


เรียนผู้ลงคะแนนเสียงโปรดอธิบายว่าทำไมคุณคิดว่าคำตอบของฉันไม่เป็นประโยชน์
Daniel Jour

1

หากสตริงจะถูกค้นหาหลายครั้ง (ปัญหา "ค้นหา" ทั่วไป) วิธีแก้ไขอาจเป็น O (1) ทางออกคือการสร้างดัชนี

เช่น :

แผนที่โดยที่ Key คือ Character and Value คือรายการดัชนีสำหรับอักขระนั้นในสตริง

ด้วยวิธีนี้การค้นหาแผนที่เดียวสามารถให้คำตอบ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.