การสร้างดัชนีทั้งหมดของลำดับโดยทั่วไปเป็นความคิดที่ไม่ดีเนื่องจากอาจต้องใช้เวลามากโดยเฉพาะอย่างยิ่งหากอัตราส่วนของตัวเลขที่จะเลือกMAX
ต่ำ (ความซับซ้อนจะถูกครอบงำโดยO(MAX)
) สิ่งนี้จะแย่ลงถ้าอัตราส่วนของตัวเลขที่จะเลือกMAX
เข้าใกล้หนึ่งเนื่องจากการลบดัชนีที่เลือกออกจากลำดับของทั้งหมดก็มีราคาแพงเช่นกัน (เราเข้าใกล้O(MAX^2/2)
) แต่สำหรับคนจำนวนน้อยสิ่งนี้มักจะใช้ได้ดีและไม่เกิดข้อผิดพลาดโดยเฉพาะ
การกรองดัชนีที่สร้างขึ้นโดยใช้คอลเลคชันก็เป็นความคิดที่ไม่ดีเช่นกันเนื่องจากใช้เวลาในการแทรกดัชนีลงในลำดับและไม่รับประกันความคืบหน้าเนื่องจากตัวเลขสุ่มเดียวกันสามารถวาดได้หลายครั้ง (แต่สำหรับจำนวนที่มากพอMAX
ก็ไม่น่าเป็นไปได้ ). สิ่งนี้อาจใกล้เคียงกับความซับซ้อน
O(k n log^2(n)/2)
โดยไม่สนใจสิ่งที่ซ้ำกันและสมมติว่าคอลเล็กชันใช้ต้นไม้เพื่อการค้นหาที่มีประสิทธิภาพ (แต่มีต้นทุนคงที่ที่สำคัญk
ในการจัดสรรโหนดต้นไม้และอาจต้องปรับสมดุลใหม่ )
อีกทางเลือกหนึ่งคือการสร้างค่าแบบสุ่มโดยไม่ซ้ำกันตั้งแต่เริ่มต้นซึ่งรับประกันความคืบหน้า นั่นหมายความว่าในรอบแรกดัชนีแบบสุ่ม[0, MAX]
จะถูกสร้างขึ้น:
items i0 i1 i2 i3 i4 i5 i6 (total 7 items)
idx 0 ^^ (index 2)
ในรอบที่สอง[0, MAX - 1]
จะถูกสร้างขึ้นเท่านั้น(เนื่องจากได้เลือกหนึ่งรายการแล้ว):
items i0 i1 i3 i4 i5 i6 (total 6 items)
idx 1 ^^ (index 2 out of these 6, but 3 out of the original 7)
จากนั้นจำเป็นต้องปรับค่าของดัชนี: หากดัชนีที่สองอยู่ในช่วงครึ่งหลังของลำดับ (หลังจากดัชนีแรก) จำเป็นต้องเพิ่มขึ้นเพื่อพิจารณาช่องว่าง เราสามารถใช้สิ่งนี้เป็นลูปทำให้เราสามารถเลือกไอเทมที่ไม่ซ้ำกันได้ตามจำนวนที่ต้องการ
สำหรับลำดับสั้น ๆ นี่เป็นO(n^2/2)
อัลกอริทึมที่ค่อนข้างเร็ว:
void RandomUniqueSequence(std::vector<int> &rand_num,
const size_t n_select_num, const size_t n_item_num)
{
assert(n_select_num <= n_item_num);
rand_num.clear();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
size_t n_where = i;
for(size_t j = 0; j < i; ++ j) {
if(n + j < rand_num[j]) {
n_where = j;
break;
}
}
rand_num.insert(rand_num.begin() + n_where, 1, n + n_where);
}
}
n_select_num
5 ของคุณอยู่ที่ไหนและn_number_num
เป็นMAX
ไฟล์. n_Rand(x)
ผลตอบแทนจำนวนเต็มสุ่ม[0, x]
(รวม) สิ่งนี้สามารถทำได้เร็วขึ้นเล็กน้อยหากเลือกรายการจำนวนมาก (เช่นไม่ใช่ 5 แต่ 500) โดยใช้การค้นหาแบบไบนารีเพื่อค้นหาจุดแทรก ในการทำเช่นนั้นเราต้องตรวจสอบให้แน่ใจว่ามีคุณสมบัติตรงตามข้อกำหนด
เราจะทำค้นหาแบบทวิภาคกับการเปรียบเทียบซึ่งเป็นเช่นเดียวกับn + j < rand_num[j]
n < rand_num[j] - j
เราจำเป็นต้องแสดงให้เห็นว่ายังคงเรียงลำดับสำหรับการเรียงลำดับrand_num[j] - j
rand_num[j]
โชคดีที่แสดงให้เห็นได้อย่างง่ายดายเนื่องจากระยะห่างต่ำสุดระหว่างสององค์ประกอบของต้นฉบับrand_num
คือหนึ่ง (ตัวเลขที่สร้างขึ้นไม่ซ้ำกันดังนั้นจึงมีความแตกต่างอย่างน้อย 1 เสมอ) ในขณะเดียวกันถ้าเราลบดัชนีj
ออกจากองค์ประกอบทั้งหมดความ
rand_num[j]
แตกต่างของดัชนีจะเท่ากับ 1 ดังนั้นในกรณีที่ "แย่ที่สุด" เราจะได้ลำดับคงที่ - แต่ไม่เคยลดลง ดังนั้นจึงสามารถใช้การค้นหาไบนารีได้โดยให้O(n log(n))
อัลกอริทึม:
struct TNeedle {
int n;
TNeedle(int _n)
:n(_n)
{}
};
class CCompareWithOffset {
protected:
std::vector<int>::iterator m_p_begin_it;
public:
CCompareWithOffset(std::vector<int>::iterator p_begin_it)
:m_p_begin_it(p_begin_it)
{}
bool operator ()(const int &r_value, TNeedle n) const
{
size_t n_index = &r_value - &*m_p_begin_it;
return r_value < n.n + n_index;
}
bool operator ()(TNeedle n, const int &r_value) const
{
size_t n_index = &r_value - &*m_p_begin_it;
return n.n + n_index < r_value;
}
};
และในที่สุดก็:
void RandomUniqueSequence(std::vector<int> &rand_num,
const size_t n_select_num, const size_t n_item_num)
{
assert(n_select_num <= n_item_num);
rand_num.clear();
for(size_t i = 0; i < n_select_num; ++ i) {
int n = n_Rand(n_item_num - i - 1);
std::vector<int>::iterator p_where_it = std::upper_bound(rand_num.begin(), rand_num.end(),
TNeedle(n), CCompareWithOffset(rand_num.begin()));
rand_num.insert(p_where_it, 1, n + p_where_it - rand_num.begin());
}
}
ฉันได้ทดสอบสิ่งนี้กับสามเกณฑ์มาตรฐาน อันดับแรกหมายเลข 3 ตัวถูกเลือกจาก 7 รายการและฮิสโตแกรมของรายการที่เลือกสะสมมากกว่า 10,000 รัน
4265 4229 4351 4267 4267 4364 4257
นี่แสดงให้เห็นว่าแต่ละรายการจาก 7 รายการถูกเลือกในจำนวนครั้งที่เท่ากันโดยประมาณและไม่มีอคติที่ชัดเจนที่เกิดจากอัลกอริทึม ลำดับทั้งหมดยังได้รับการตรวจสอบความถูกต้อง (ความเป็นเอกลักษณ์ของเนื้อหา)
เกณฑ์มาตรฐานที่สองเกี่ยวข้องกับการเลือก 7 หมายเลขจาก 5,000 รายการ เวลาของอัลกอริทึมหลายเวอร์ชันถูกสะสมมากกว่า 10,000,000 รัน b1
ผลที่จะได้แสดงในความคิดเห็นในรหัสเป็น อัลกอริทึมรุ่นธรรมดาเร็วกว่าเล็กน้อย
เกณฑ์มาตรฐานที่สามเกี่ยวข้องกับการเลือก 700 หมายเลขจาก 5,000 รายการ เวลาของอัลกอริทึมหลายเวอร์ชันถูกสะสมอีกครั้งคราวนี้มีการรันมากกว่า 10,000 ครั้ง b2
ผลที่จะได้แสดงในความคิดเห็นในรหัสเป็น อัลกอริทึมรุ่นการค้นหาแบบไบนารีตอนนี้เร็วกว่ารุ่นธรรมดามากกว่าสองเท่า
วิธีที่สองเริ่มเร็วขึ้นสำหรับการเลือกมากกว่า cca 75 รายการบนเครื่องของฉัน (โปรดทราบว่าความซับซ้อนของอัลกอริทึมใดไม่ขึ้นอยู่กับจำนวนรายการMAX
)
เป็นที่น่าสังเกตว่าอัลกอริทึมข้างต้นสร้างตัวเลขสุ่มจากน้อยไปหามาก แต่จะเป็นการง่ายที่จะเพิ่มอาร์เรย์อื่นซึ่งจะมีการบันทึกตัวเลขตามลำดับที่สร้างขึ้นและส่งกลับมาแทน (โดยมีค่าใช้จ่ายเพิ่มเติมเล็กน้อยO(n)
) ไม่จำเป็นต้องสับเปลี่ยนเอาต์พุตซึ่งจะช้ากว่ามาก
โปรดทราบว่าแหล่งที่มาอยู่ใน C ++ ฉันไม่มี Java ในเครื่องของฉัน แต่แนวคิดควรชัดเจน
แก้ไข :
เพื่อความบันเทิงฉันยังได้ใช้วิธีการสร้างรายการพร้อมดัชนีทั้งหมด
0 .. MAX
เลือกแบบสุ่มและลบออกจากรายการเพื่อรับประกันความเป็นเอกลักษณ์ เนื่องจากฉันเลือกค่อนข้างสูงMAX
(5,000) ประสิทธิภาพจึงเป็นหายนะ:
std::vector<int> all_numbers(n_item_num);
std::iota(all_numbers.begin(), all_numbers.end(), 0);
for(size_t i = 0; i < n_number_num; ++ i) {
assert(all_numbers.size() == n_item_num - i);
int n = n_Rand(n_item_num - i - 1);
rand_num.push_back(all_numbers[n]);
all_numbers.erase(all_numbers.begin() + n);
}
ฉันยังใช้วิธีการนี้กับset
(คอลเลกชัน C ++) ซึ่งมาอันดับสองในเกณฑ์มาตรฐานb2
โดยช้ากว่าวิธีการค้นหาแบบไบนารีเพียงประมาณ 50% นั่นเป็นที่เข้าใจได้เนื่องจากการset
ใช้ต้นไม้ไบนารีซึ่งต้นทุนการแทรกนั้นคล้ายกับการค้นหาแบบไบนารี ข้อแตกต่างเพียงอย่างเดียวคือโอกาสในการได้รับรายการที่ซ้ำกันซึ่งจะทำให้ความคืบหน้าช้าลง
std::set<int> numbers;
while(numbers.size() < n_number_num)
numbers.insert(n_Rand(n_item_num - 1));
rand_num.resize(numbers.size());
std::copy(numbers.begin(), numbers.end(), rand_num.begin());
รหัสที่มาเต็มรูปแบบที่นี่