如果可以知道每门课程的学生人数,那么知道是否有一门课程的学生人数 >= N/2 就足够了。在这种情况下,最坏情况下的复杂度为O(N)。
如果无法知道每门课程的学生人数,那么您可以使用更改后的快速排序。在每个周期中,您随机选择一名学生并将其他学生分成同学和非同学。如果同学人数 >= N/2,则您停止,因为您有答案,否则您分析非同学分区。如果该分区中的学生人数 = N/2,否则您从非同学分区中选择另一个学生并仅使用非同学重复所有内容-classmates 元素。
我们从快速排序算法中得到的只是我们划分学生的方式。上述算法与排序无关。在伪代码中它看起来像这样(为了清楚起见,数组索引从 1 开始):
Student[] students = all_students;
int startIndex = 1;
int endIndex = N; // number of students
int i;
while(startIndex <= N/2){
endIndex = N; // this index must point to the last position in each cycle
students.swap(startIndex, start index + random_int % (endIndex-startIndex));
for(i = startIndex + 1; i < endIndex;){
if(students[startIndex].isClassmatesWith(students[i])){
i++;
}else{
students.swap(i,endIndex);
endIndex--;
}
}
if(i-startIndex >= N/2){
return true;
}
startIndex = i;
}
return false;
算法开始前的分区情况就这么简单:
| all_students_that_must_be_analyzed |
在第一次运行期间,学生集将按以下方式划分:
| classmates | to_be_analyzed | not_classmates |
在之后的每次运行中,学生集将按如下方式划分:
| to_ignore | classmates | to_be_analyzed | not_classmates |
在每次运行结束时,学生集将按以下方式进行划分:
| to_ignore | classmates | not_classmates |
此时我们需要检查 classmates 分区是否有超过 N/2 个元素。如果有,那么我们有一个肯定的结果,如果没有,我们需要检查 not_classmates 分区是否有 >= N/2 个元素。如果有,那么我们需要进行另一次运行,否则我们的结果是否定的。
关于复杂性
更深入地思考上述算法的复杂性,影响它的主要因素有两个,分别是:
- 每门课程的学生人数(无需知道该人数即可让算法发挥作用)。
- 算法每次迭代中找到的平均同学数。
算法的一个重要部分是要分析的学生的随机选择。
最坏的情况是每门课程有 1 名学生。在这种情况下(出于显而易见的原因,我会说)复杂性将是O(N^2)。如果课程的学生人数不同,则不会发生这种情况。
最坏情况的一个例子是,假设我们有 10 名学生,10 门课程,每门课程有 1 名学生。第一次检查 10 名学生,第二次检查 9 名学生,第三次检查 8 名学生,以此类推。这带来了O(N^2) 的复杂性。
最佳情况是当您选择的第一个学生在一个学生人数 >= N/2 的课程中。在这种情况下,复杂度将是 O(N),因为它会在第一次运行时停止。
最好的情况是,我们有 10 名学生,其中 5 人(或更多)是同学,在第一次运行中,我们从这 5 名学生中挑选一个。在这种情况下,我们将只检查1次同学,找到5个同学,并返回true。
平均案例场景是最有趣的部分(并且更接近真实场景)。在这种情况下,需要进行一些概率计算。
首先,特定课程的学生被选中的机会是[number_of_students_in_the_course] / N。这意味着,在第一次运行中,更有可能选择一个有很多同学的学生。
话虽如此,让我们考虑这样一种情况,即每次迭代中找到的同学的平均数量小于 N/2(快速排序的平均情况下每个分区的长度也是如此)。假设在每次迭代中找到的同学的平均数量是剩余 M 个学生(不是先前选择的学生的同学)的 10%(为便于计算而采用的数量)。在这种情况下,每次迭代我们都会有这些 M 值:
M1 = N - 0.1*N = 0.9*N
M2 = M1 - 0.1*M1 = 0.9*M1 = 0.9*0.9*N = 0.81*N
-
M3 = M2 - 0.1*M2 = 0.9*M2 = 0.9*0.81*N = 0.729*N 我会将其四舍五入为 0.73*N 以便于计算
M4 = 0.9*M3 = 0.9*0.73*N = 0.657*N ~= 0.66*N
M5 = 0.9*M4 = 0.9*0.66*N = 0.594*N ~= 0.6*N
M6 = 0.9*M5 = 0.9*0.6*N = 0.54*N
M7 = 0.9*M6 = 0.9*0.54*N = 0.486*N ~= 0.49*N
- 算法停止,因为我们还有 49% 的剩余学生,而且我们的同学不能超过 N/2。
显然,在平均同学比例较小的情况下,迭代次数会更多,但是结合第一个事实(学生多的课程中的学生在早期迭代中被选中的概率更高),复杂度将趋向于O(N),迭代次数(在伪代码的外循环中)将(或多或少)恒定且不依赖于 N。
为了更好地解释这种情况,让我们使用更大(但更现实)的数字和超过 1 个分布。假设我们有 100 名学生(为了计算简单而取的数字),这些学生以下列(假设的)方式之一分布在课程中(这些数字只是为了解释目的而排序,它们不是必需的算法工作):
- 50、30、10、5、1、1、1、1、1
- 35、27、25、10、5、1、1、1
- 11、9、9、8、7、7、5、5、5、5、5、5、5、5、5、3、1
给出的数字也是(在这种特殊情况下)课程中的学生(不是特定学生,只是该课程的学生)在第一次运行中被选中的概率。第一种情况是我们有一半学生参加的课程。第二种情况是我们没有半数学生的课程,但有很多学生的多于一门课程。第三种情况是我们的课程分布相似。
在第一种情况下,第一门课程的学生有 50% 的概率被选中,第二门课程的学生有 30% 的概率被选中,第三门课程的学生有 10% 的概率课程被选中,第 4 门课程的学生被选中的概率为 5%,第 5 门课程的学生被选中的概率为 1%,依此类推,第 6、第 7、第 8 和第 9 门课程。第一个案例的学生提前被选中的概率更高,如果该课程的学生在第一次运行中没有被选中,那么它在第二次运行中被选中的概率只会增加。例如,假设在第一次运行中选择了第二门课程的学生。 30% 的学生将被“移除”(如“不再考虑”)并且不会在第二轮分析中进行分析。在第二轮中,我们将剩下 70 名学生。第二轮从第一门课程中挑选学生的概率是 5/7,超过 70%。让我们假设 - 运气不好 - 在第二轮比赛中,一名来自第三门课程的学生被选中。在第三轮中,我们将剩下 60 名学生,第一门课程的学生在第三轮中被选中的概率为 5/6(超过 80%)。我想说我们可以认为我们的厄运在第三轮结束了,第一门课程的学生被选中,该方法返回true :)
对于第 2 和第 3 种情况,我将遵循每次运行的概率,只是为了计算简单。
在第 2 种情况下,我们将在第 1 次运行中选择来自第 1 门课程的学生。由于同学的数量不是 false。
同样的情况发生在第 3 种情况下(我们在每次运行中平均“移除”剩余学生的 10%),但步骤更多。
最终考虑
无论如何,最坏情况下算法的复杂度是O(N^2)。 平均案例场景很大程度上基于概率,并且倾向于从参加者众多的课程中提早挑选学生。这种行为往往会将复杂性降低到O(N),这是我们在最佳情况中也有的复杂性。
算法测试
为了测试算法的理论复杂度,我用 C# 编写了以下代码:
public class Course
{
public int ID { get; set; }
public Course() : this(0) { }
public Course(int id)
{
ID = id;
}
public override bool Equals(object obj)
{
return (obj is Course) && this.Equals((Course)obj);
}
public bool Equals(Course other)
{
return ID == other.ID;
}
}
public class Student
{
public int ID { get; set; }
public Course Class { get; set; }
public Student(int id, Course course)
{
ID = id;
Class = course;
}
public Student(int id) : this(id, null) { }
public Student() : this(0) { }
public bool IsClassmatesWith(Student other)
{
return Class == other.Class;
}
public override bool Equals(object obj)
{
return (obj is Student) && this.Equals((Student)obj);
}
public bool Equals(Student other)
{
return ID == other.ID && Class == other.Class;
}
}
class Program
{
static int[] Sizes { get; set; }
static List<Student> Students { get; set; }
static List<Course> Courses { get; set; }
static void Initialize()
{
Sizes = new int[] { 2, 10, 100, 1000, 10000, 100000, 1000000 };
Students = new List<Student>();
Courses = new List<Course>();
}
static void PopulateCoursesList(int size)
{
for (int i = 1; i <= size; i++)
{
Courses.Add(new Course(i));
}
}
static void PopulateStudentsList(int size)
{
Random ran = new Random();
for (int i = 1; i <= size; i++)
{
Students.Add(new Student(i, Courses[ran.Next(Courses.Count)]));
}
}
static void Swap<T>(List<T> list, int i, int j)
{
if (i < list.Count && j < list.Count)
{
T temp = list[i];
list[i] = list[j];
list[j] = temp;
}
}
static bool AreHalfOfStudentsClassmates()
{
int startIndex = 0;
int endIndex;
int i;
int numberOfStudentsToConsider = (Students.Count + 1) / 2;
Random ran = new Random();
while (startIndex <= numberOfStudentsToConsider)
{
endIndex = Students.Count - 1;
Swap(Students, startIndex, startIndex + ran.Next(endIndex + 1 - startIndex));
for (i = startIndex + 1; i <= endIndex; )
{
if (Students[startIndex].IsClassmatesWith(Students[i]))
{
i++;
}
else
{
Swap(Students, i, endIndex);
endIndex--;
}
}
if (i - startIndex + 1 >= numberOfStudentsToConsider)
{
return true;
}
startIndex = i;
}
return false;
}
static void Main(string[] args)
{
Initialize();
int studentsSize, coursesSize;
Stopwatch stopwatch = new Stopwatch();
TimeSpan duration;
bool result;
for (int i = 0; i < Sizes.Length; i++)
{
for (int j = 0; j < Sizes.Length; j++)
{
Courses.Clear();
Students.Clear();
studentsSize = Sizes[j];
coursesSize = Sizes[i];
PopulateCoursesList(coursesSize);
PopulateStudentsList(studentsSize);
Console.WriteLine("Test for {0} students and {1} courses.", studentsSize, coursesSize);
stopwatch.Start();
result = AreHalfOfStudentsClassmates();
stopwatch.Stop();
duration = stopwatch.Elapsed;
var studentsGrouping = Students.GroupBy(s => s.Class);
var classWithMoreThanHalfOfTheStudents = studentsGrouping.FirstOrDefault(g => g.Count() >= (studentsSize + 1) / 2);
Console.WriteLine(result ? "At least half of the students are classmates." : "Less than half of the students are classmates");
if ((result && classWithMoreThanHalfOfTheStudents == null)
|| (!result && classWithMoreThanHalfOfTheStudents != null))
{
Console.WriteLine("There is something wrong with the result");
}
Console.WriteLine("Test duration: {0}", duration);
Console.WriteLine();
}
}
Console.ReadKey();
}
}
执行时间符合平均案例场景的预期。随意使用代码,您只需复制并粘贴它,它应该可以工作。