从整数列表中获取模式

3
我需要编写一个程序来查找众数,即整数或多个整数的出现最多次数。
例如:1,2,3,4,1,10,4,23,12,4,1的众数为1和4。
我不确定应该使用什么样的算法。我很难想到一个行之有效的方案。
我想可能需要用到某种频率表,在数组中遍历并创建一个链表。如果链表中不存在该值,则将其添加到链表中,如果存在,则将该值加1。
所以,如果我有上面相同的东西。循环遍历1、2、3、4、1、10、4、23、12、4、1
然后列表为空,因此添加数字=1且值=1的节点。2不存在,因此添加数字=2且值=1等等。
到达1并且1已经存在,所以value=2。
我必须循环遍历数组并每次遍历链表才能找到该值。
完成后,我会遍历链表并创建一个新的链表来保存众数。所以我将头部设置为第一个元素,即1。然后,我遍历包含出现次数的链表并比较这些值。如果当前节点的出现次数大于当前最高值,则将头部设置为该节点。如果它等于最高值,则将该节点添加到模式链表中。
完成后,我循环遍历模式列表并打印值。
不确定这是否有效。有人看到任何问题吗?有更简单的方法吗?我也在想哈希表,但不确定如何在C中实现。

2
你需要知道的第一件事是整数范围是否有任何限制:它们是1到100之间的整数吗?它们都是正数吗?它们都是有效的32位整数吗?有许多解决方法,但最有效的方法肯定取决于问题的限制条件。 - James McNellis
@James,我并不是真的要用最有效的方式来解决这个问题。如果你在这个级别上提交一个使用二叉树或使用假设有限范围来索引数组进行盲目增量的作业,你几乎肯定会被标记为可能作弊。结果可能因人而异。 - paxdiablo
1
取决于具体情况。我之前上过其他编程课程,也会使用不同的网络语言进行编程。所以,我之前已经学过二叉树了。没有什么新的东西,只是需要用不同的C语言实现它。因此,当涉及到在C语言中编程时,语法确实有所不同,但如果你不挑战自己,就永远无法学习。 - Matt
5个回答

5
如果您可以将整个整数列表保存在内存中,您可以首先对列表进行排序,这将使重复值相邻。然后,您可以对排序后的列表进行单次遍历以查找模式。这样,您只需要跟踪到目前为止看到的模式的最佳候选者,以及当前值已经被看到的次数即可。

你可以使用stdlib函数qsort来进行排序。 - strager
是的,那样做可以。基本上就是我上面写的。对列表进行排序。将模式设置为第一个元素。开始扫描不同整数并计数。如果计数更高,则将模式设置为新整数;如果相等,则将其添加到模式的链表中。这可能会更容易,因为我可以像strager提到的那样使用qsort。谢谢。 - Matt
排序将整个计算复杂度提高到 O(N * lgN),这是不必要的;而众数可以在 O(N)内计算出来。 - Arun

2
你现有的算法对于作业任务来说已经很好了。你可以做各种优化代码的事情,例如:
  • 使用二叉树提高效率,
  • 使用计数的数组,其中索引是数字(假设数字范围有限)。
但我认为在这种情况下它们并不是必要的。对于作业,意图只是展示你理解如何编程,而不是你知道各种技巧来挤出最后一点性能。你的教育者更关注可读性强、结构良好的代码,而不是巧妙的优化。
下面我将描述我会做的事情。你可以自由地使用我的建议,取决于你想要多少满意度。我只提供伪代码,这是我对作业问题的标准做法。
我会从一个结构开始,包含一个数字、一个计数和一个下一个指针(用于你的链表),以及指向第一个指针的全局指针:
typedef struct sElement {
    int number;
    int count;
    struct sElement *next;
} tElement;
tElement first = NULL;

然后创建一些函数来创建和使用列表:
tElement *incrementElement (int number);
tElement *getMaxCountElement (void);
tElement *getNextMatching (tElement *ptr, int count);

这些函数将会:

  • 增加元素的计数(如果该元素不存在,则创建并将计数设置为1)。
  • 扫描所有元素,返回最大计数。
  • 从给定点开始获取下一个匹配计数的元素指针,或者是NULL(如果没有更多匹配的元素)。

每个函数的伪代码:

def incrementElement (number):
    # Find matching number in list or NULL.

    set ptr to first
    while ptr is not NULL:
        if ptr->number is equal to number:
            return ptr
        set ptr to ptr->next

    # If not found, add one at start with zero count.

    if ptr is NULL:
        set ptr to newly allocated element
        set ptr->number to number
        set ptr->count to 0
        set ptr->next to first
        set first to ptr            

    # Increment count.

    set ptr->count to ptr->count + 1

 

def getMaxCountElement (number):
    # List empty, no mode.

    if first is NULL:
        return NULL

    # Assume first element is mode to start with.

    set retptr to first

    # Process all other elements.

    set ptr to first->next
    while ptr is not NULL:
        # Save new mode if you find one.

        if ptr->count is greater than retptr->count:
            set retptr to ptr
        set ptr to ptr->next

    # Return actual mode element pointer.

    return retptr

 

def getNextMatching (ptr, number):
    # Process all elements.

    while ptr is not NULL:
        # If match on count, return it.

        if ptr->number is equal to number:
            return ptr
        set ptr to ptr->next

    # Went through whole list with no match, return NULL.

    return NULL

那么你的主程序将变成:

# Process all the numbers, adding to (or incrementing in) list .

for each n in numbers to process:
    incrementElement (n)

# Get the mode quantity, only look for modes if list was non-empty.

maxElem = getMaxCountElement ()
if maxElem is not NULL:
    # Find the first one, whil exists, print and find the next one.

    ptr = getNextMatching (first, maxElem->count)
    while ptr is not NULL:
        print ptr->number
        ptr = getNextMatching (ptr->next, maxElem->count)

是的,我喜欢尝试挑战自己。虽然在某些情况下我只是为了完成作业而编程,但我不喜欢糟糕的编程。我喜欢尽可能地优化我的代码。我不会为此而发疯,但我喜欢在给定的时间内做到最佳优化。谢谢大家,我觉得我从这里得到了很多可以用来工作的东西。 - Matt
通常一眼之下很难判断你的伪代码到底是不是Python... :-D - James McNellis
这就是为什么我喜欢Python。它非常适合教育年轻人如何编程。至少在引入所有那些lambda垃圾之前是这样的 :-) 不过,我可以只使用简单的部分来教学。很多时候,我实际上会用Python编写代码,检查它,然后返回并更改行,例如el = arr [7]将el设置为arr的第八个元素,以使其更伪代码化。 - paxdiablo

1

我会选择一个基于哈希表的简单解决方案。

一个包含数字和相应频率的哈希表结构。另外还有一个指向哈希桶中下一个元素的指针,用于链式存储。

struct ItemFreq {
    struct ItemFreq * next_;
    int    number_;
    int    frequency_;
};

处理开始于

max_freq_so_far = 0;

该算法遍历数字列表。对于每个number,都会查找哈希表中是否存在一个ItemFreq元素x,满足x.number_ == number

  • 如果找不到这样的x,则创建一个ItemFreq元素,如{ number_=number,frequency_=1 },并将其插入哈希表中。
  • 如果找到一些x,则将其frequency_增加。
  • 如果frequency_ > max_freq_so_far,则max_freq_so_far = frequency

一旦完成遍历完数字列表,我们将遍历哈希表,并打印其中frequency_ == max_freq_so_farItemFreq项。

该算法的复杂度为O(N),其中N是输入列表中的项目数。

如果想要一个简单而优雅的哈希表构建方式,请参考K&R (The C Programming Language)第6.6节


看起来对我来说像是一个链表。 - Matt
@Matt:是的,没错,哈希到同一个桶中的“数字”会链接成一个列表。 - Arun
我不确定我理解了。据我所知,哈希表应该是O(1)的,至少我认为是这样。但我确实使用了链表来创建一个频率表。因此它的时间复杂度是O(N)。尽管如此,速度仍然相当快。 - Matt
@Matt:是的,在哈希表中进行lookupinsert操作的时间复杂度应该是期望的O(1)。而O(N)的时间复杂度是因为需要检查每个输入数字(共有N个数字)。对于每个输入数字,我们都要进行一次哈希表的查找/插入操作,这个操作的时间复杂度也是O(1)。因此,整个过程的时间复杂度是O(N)。 - Arun
是的,这正是我想的。我现在得离开哈希表,稍后再回来看看它。至少我学会了C语言中的链表。耶! - Matt

1

如果数字范围事先已知,并且是一个合理的数字,您可以为计数器分配足够大的数组,然后只需执行count[i] += 1

如果数字范围事先不知道,或者对于数组的朴素使用来说太大了,那么您可以维护一个二叉树来维护计数器。这将比链表少得多的搜索次数。无论哪种方式,您都必须遍历数组或树,并构建从最高到最低计数的排序。再次推荐使用树,但您的列表解决方案也可以工作。

另一个有趣的选择可能是在提取阶段使用优先队列。完成计数器列表后,遍历树并将每个值插入优先级等于其计数的优先队列中。然后,您只需从优先队列中拉出值,直到计数下降即可。


二叉树肯定可以用。没听说过优先队列。老实说,我不知道我的老师会为测试用例做什么。我只知道他可能会随机生成一个包含10,000个整数的列表并将其传递到程序中。 - Matt
如果你不知道潜在的范围,那么二叉树可能是适合的。至于优先队列,可以点击链接,在底部找到一些实现,你可以快速地将其用于作业。 - JUST MY correct OPINION

0

这个响应是保罗·库林尼维茨的想法示例:

int CompInt(const void* ptr1, const void* ptr2) {
  const int a = *(int*)ptr1;
  const int b = *(int*)ptr2;
  if (a < b) return -1;
  if (a > b) return +1;
  return 0;
}

// This function leave the modes in output and return the number
// of modes in output. The output pointer should be available to
// hold at least n integers.
int GetModes(const int* v, int n, int* output) {
  // Sort the data and initialize the best result.
  qsort(v, v + n, CompInt);
  int outputSize = 0;

  // Loop through elements while there are not exhausted.
  // (look there is no ++i after each iteration).
  for (int i = 0; i < n;) {
    // This is the begin of the new group.
    const int begin = i;

    // Move the pointer until there are no more equal elements.
    for (; i < n && v[i] == v[begin]; ++i);

    // This is one-past the last element in the current group.
    const int end = i;

    // Update the best mode found until now.
    if (end - begin > best) {
      best = end - begin;
      outputSize = 0;
    }
    if (end - begin == best)
      output[outputSize++] = v[begin];
  }
  return outputSize;
}

3
问题是关于 C 语言,不是 C++。 - strager
完成,将示例更改为使用C而不是C ++。 - jbernadas
排序需要整个计算时间为O(N * lgN),这是不必要的;而众数可以在O(N)的时间内计算出来。 - Arun
我猜测它在使用哈希。虽然我知道哈希算法可以得到非常好的结果,但我倾向于在作业或非常简单的任务中不使用哈希算法,因为这样看起来更简单,并且不依赖分摊分析。 - jbernadas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接