การสร้างคีย์เรียงลำดับเมื่อเรียงลำดับรายการใหม่


11

เรามีรายการจำนวนหนึ่งที่ผู้ใช้จะสามารถจัดระเบียบตามลำดับที่ต้องการได้ ชุดของไอเท็มไม่ได้เรียงลำดับ แต่แต่ละไอเท็มมีคีย์การเรียงซึ่งสามารถแก้ไขได้

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

อัลกอริทึมตัวอย่างจะมีแต่ละคีย์การเรียงลำดับเป็นหมายเลขทศนิยมและเมื่อวางรายการระหว่างสองรายการให้ตั้งค่าคีย์การเรียงเป็นค่าเฉลี่ย การวางรายการแรกหรือสุดท้ายจะใช้ค่านอกสุด + - 1

ปัญหาที่นี่คือความแม่นยำจุดลอยตัวอาจทำให้การเรียงลำดับล้มเหลว การใช้จำนวนเต็มสองจำนวนแทนจำนวนเศษส่วนในทำนองเดียวกันอาจทำให้ตัวเลขมีขนาดใหญ่จนไม่สามารถแสดงได้อย่างแม่นยำในประเภทตัวเลขปกติ (เช่นเมื่อถ่ายโอนเป็น JSON) เราไม่ต้องการใช้ BigInts

มีอัลกอริทึมที่เหมาะสมสำหรับสิ่งนี้ที่จะใช้งานได้ตัวอย่างเช่นการใช้สตริงซึ่งจะไม่ได้รับผลกระทบจากข้อบกพร่องเหล่านี้หรือไม่?

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


สตริงเป็นตัวเลือกที่ชัดเจนเพราะคุณสามารถเพิ่มตัวอักษรต่อท้ายเพื่อแยกไปสองทาง ที่กล่าวว่าฉันรู้สึกว่ามีวิธีที่ดีกว่าในการเข้าถึงนี้
Robert Harvey

จากด้านบนของหัวของฉันฉันไม่เห็นวิธีที่จะทำให้มันทำงานโดยใช้สตริงโดยไม่ต้องแก้ไขคีย์รายการอื่น
Sampo

3
ปัญหาที่คุณกำลังอธิบายเรียกว่าปัญหาการบำรุงรักษาคำสั่ง
นาธานเมอร์ริลล์

1
เหตุใดคุณจึงกังวลว่าจะไม่แก้ไขรายการอื่นในรายการ
GER

1
วิธีที่คุณทำให้มันทำงานกับสตริงเป็นเช่นนี้ A, B, C- A, AA, B, C- -A, AA, AB, B, C A, AA, AAA, AAB, AAC, AB, AC, B, Cแน่นอนคุณอาจต้องการเว้นวรรคจดหมายของคุณให้มากขึ้นเพื่อที่ว่าสายอักขระจะไม่เติบโตอย่างรวดเร็ว แต่ก็สามารถทำได้
Robert Harvey

คำตอบ:


4

เป็นบทสรุปของความคิดเห็นและคำตอบทั้งหมด:

TL; DR - การใช้หมายเลขทศนิยมที่มีความแม่นยำสองเท่าพร้อมกับอัลกอริธึมที่เสนอมาครั้งแรกควรจะเพียงพอสำหรับความต้องการในทางปฏิบัติ (อย่างน้อยต้องสั่งด้วยตนเอง) การดูแลรักษารายการสั่งซื้อแยกต่างหากขององค์ประกอบควรได้รับการพิจารณาเช่นกัน วิธีแก้ปัญหาการเรียงลำดับอื่น ๆ ค่อนข้างยุ่งยาก

การดำเนินการที่เป็นปัญหาสองอย่างคือการแทรกรายการที่จุดเริ่มต้น / สิ้นสุดซ้ำแล้วซ้ำอีกและการแทรกหรือย้ายรายการไปที่จุดเดิมซ้ำ ๆ (เช่นด้วยองค์ประกอบสามอย่างซ้ำ ๆ ย้ายองค์ประกอบที่สามระหว่างสองครั้งแรกหรือเพิ่มองค์ประกอบใหม่ซ้ำ ๆ ธาตุ).

จากมุมมองทางทฤษฎี (เช่นอนุญาตให้มีการเรียงลำดับใหม่อีกครั้ง) วิธีเดียวที่ฉันคิดได้คือใช้จำนวนเต็มไม่ จำกัด จำนวนสองจำนวนเป็นเศษส่วน a / b สิ่งนี้ช่วยให้ความแม่นยำไม่มีที่สิ้นสุดสำหรับเม็ดมีดกลาง แต่ตัวเลขอาจมีขนาดใหญ่ขึ้นเรื่อย ๆ

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

จำนวนเต็มจะต้องเลือกระยะห่างเริ่มต้นสำหรับการจัดเรียงคีย์ซึ่ง จำกัด จำนวนแทรกกลางที่คุณสามารถทำได้ หากคุณเว้นวรรคในการเรียงลำดับคีย์ 1024 แยกกันคุณสามารถดำเนินการแทรกกลางคันกรณีที่แย่ที่สุด 10 รายการก่อนที่คุณจะมีหมายเลขติดกัน การเลือกระยะห่างเริ่มต้นที่ใหญ่กว่า จำกัด จำนวนเม็ดมีดแรก / เม็ดสุดท้ายที่คุณสามารถทำได้ เมื่อใช้จำนวนเต็ม 64 บิตคุณจะถูก จำกัด การดำเนินการ ~ 63 ด้วยวิธีใดวิธีหนึ่งซึ่งคุณต้องแยกระหว่างการแทรกกลางและการแทรกก่อน / ท้ายสุดก่อน

การใช้ค่าทศนิยมจะลบความจำเป็นในการเลือกระยะห่างที่สำคัญ อัลกอริทึมง่าย:

  1. องค์ประกอบแรกที่แทรกมีคีย์การเรียงลำดับ 0.0
  2. องค์ประกอบที่แทรก (หรือย้าย) ครั้งแรกหรือครั้งสุดท้ายมีคีย์การเรียงลำดับขององค์ประกอบแรก - 1.0 หรือองค์ประกอบสุดท้าย + 1.0 ตามลำดับ
  3. องค์ประกอบที่แทรก (หรือย้าย) ระหว่างสององค์ประกอบมีคีย์การเรียงลำดับเท่ากับค่าเฉลี่ยของทั้งสอง

การใช้ความแม่นยำสองเท่าช่วยให้แทรกมิด - อินเทอร์เซ็ทสุดเคส 52 อันและไม่มีที่สิ้นสุดอย่างมีประสิทธิภาพ (ประมาณ 1e15) เม็ดมีดแรก / เม็ดสุดท้าย ในทางปฏิบัติเมื่อเคลื่อนย้ายไอเท็มรอบอัลกอริทึมควรแก้ไขตัวเองด้วยตนเองทุกครั้งที่คุณย้ายไอเท็มครั้งแรกหรือครั้งสุดท้ายมันจะขยายช่วงที่สามารถใช้ได้

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


1

ฉันเขียนโซลูชันใน TypeScript ตามสรุปของ @ Sampo รหัสสามารถพบได้ด้านล่าง

ได้รับข้อมูลเชิงลึกสองทาง

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

  • ทุกๆ 1074: การแบ่งจุดกึ่งกลางเราจำเป็นต้องทำให้ช่วงพอยต์เป็นปกติ เราตรวจจับสิ่งนี้โดยการตรวจสอบว่าจุดกึ่งกลางใหม่สนองค่าคงที่หรือไม่

    a.sortKey < m && m < b.sortKey

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

  • การเรียงลำดับการทำให้เป็นมาตรฐานคีย์นั้นหายากอย่างไม่น่าเชื่อ คุณจะตัดจำหน่ายต้นทุนนี้ไปยังจุดที่การทำให้เป็นมาตรฐานไม่น่าสังเกต แม้ว่าฉันจะระวังวิธีนี้หากคุณมีองค์ประกอบมากกว่า 1,000 รายการ


export interface HasSortKey {
  sortKey: number;
}

function normalizeList<T extends HasSortKey>(list: Array<T>) {
  const normalized = new Array<T>(list.length);
  for (let i = 0; i < list.length; i++) {
    normalized[i] = { ...list[i], sortKey: i };
  }
  return normalized;
}

function insertItem<T extends HasSortKey>(
  list: Array<T>,
  index: number,
  item: Partial<T>
): Array<T> {
  if (list.length === 0) {
    list.push({ ...item, sortKey: 0 } as T);
  } else {
    // list is non-empty

    if (index === 0) {
      list.splice(0, 0, { ...item, sortKey: list[0].sortKey - 1 } as T);
    } else if (index < list.length) {
      // midpoint, index is non-zero and less than length

      const a = list[index - 1];
      const b = list[index];

      const m = (a.sortKey + b.sortKey) / 2;

      if (!(a.sortKey < m && m < b.sortKey)) {
        return insertItem(normalizeList(list), index, item);
      }

      list.splice(index, 0, { ...item, sortKey: m } as T);
    } else if (index === list.length) {
      list.push({ ...item, sortKey: list[list.length - 1].sortKey + 1 } as T);
    }
  }
  return list;
}

export function main() {
  const normalized: Array<number> = [];

  let list: Array<{ n: number } & HasSortKey> = [];

  list = insertItem(list, 0, { n: 0 });

  for (let n = 1; n < 10 * 1000; n++) {
    const list2 = insertItem(list, 1, { n });
    if (list2 !== list) {
      normalized.push(n);
    }
    list = list2;
  }

  let m = normalized[0];

  console.log(
    normalized.slice(1).map(n => {
      const k = n - m;
      m = n;
      return k;
    })
  );
}

0

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


1
คุณไม่สามารถหาคีย์ที่อยู่ข้างหน้าคีย์สตริงอื่นได้เสมอ
Sampo

-1

ใช้จำนวนเต็มและกำหนดคีย์การเรียงลำดับสำหรับรายการเริ่มต้นให้เป็น 500 * หมายเลขรายการ เมื่อแทรกระหว่างรายการคุณสามารถใช้ค่าเฉลี่ย วิธีนี้จะช่วยให้การแทรกจำนวนมากเริ่มต้นด้วย


2
นี่มันแย่กว่าการใช้โฟลต ระยะห่างเริ่มต้น 500 อนุญาตให้แทรกจุดกึ่งกลาง 8-9 เท่านั้น (2 ^ 9 = 512) ในขณะที่ทุ่นคู่อนุญาตให้ประมาณ 50 โดยไม่มีปัญหาในการเลือกระยะห่างเริ่มต้น
Sampo

ใช้ 500 ช่องว่างและลอย!
Rob Mulder

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