Пару статей назад я уже рассматривала один из алгоритмов Бойера-Мура, с помощью которого можно было найти подстроку в строке.

Сегодня хочу поболтать об алгоритме большинства голосов, который позволяется найти сильно преобладающий элемент последовательности, встречающийся более n/2 раз, где n - длина последовательности.

Предлагаю сразу использовать его на примере задачи «Majority Element» с leetcode.

Условие здесь: https://leetcode.com/problems/majority-element/description/

Кстати, у меня есть телеграм-канал, где пишу подходы к решениям всяких задачек с LeetCode, там больше разборов конкретных задач, чем здесь, потому что не всегда нужна статья. В общем, если интересно - жду здесь - t.me/crushiteasy :)

Возвращаемся к Муру!

Кратко: на вход мы получаем массив, состоящий из чисел. Нужно найти число, которое встречается наибольшее количество раз и занимает больше половины элементов массива (n/2), т.е.

  • [1,1,2] - преобладающий элемент 1

  • [3,10,56,3] - преобладающего элемента нет

  • [3,10,35,3,3] - преобладающий элемент 3

Ограничения c leetcode: 

  • Длина массива от 1 до 5 * 10^4

  • Значения массива от -10^9 до 10^9

Вообще, задачу можно решить и без алгоритма с использованием, например хэш-таблицы, куда мы будем записывать элемент и кол-во раз, которое он встречается. А потом пройдемся по ней и вернем то число, которое встречается чаще.

Код на Java выглядел бы так:

public int majorityElement(int[] nums) {

  Map<Integer, Integer> majorMap = new HashMap(); 

  for (int i = 0; i < nums.length; i++) {

      majorMap.put(nums[i], majorMap.getOrDefault(nums[i], 0) + 1); 

  }

   int majorElement = 0;

   int maxMajority = 0;

   for (Map.Entry<Integer, Integer> entry: majorMap.entrySet()) {

      int currElement = entry.getKey();

      int majority = entry.getValue();

      if (majority > maxMajority) {

         maxMajority = majority;

         majorElement = currElement;

      }

   }

   return majorElement;

}


Временная сложность такого алгоритма составит О(n), поскольку мы один раз проходимся по массиву - O(n) и один раз по хэш-таблице O(m), т.е. Общая сложность O(n+m)~ O(n)

Пространственная сложность: O(n), так как количество элементов в хэш-таблицы будет зависеть от размера входного массива.

Что позволяет сделать алгоритм Бойера-Мура в данном случае?

По временной сложности мы ничего не выиграем, т.к. нам в любом случае придется обходить массив, т.е. она останется равной О(n), а вот пространственная сложность улучшится до O(1), т.к. нам понадобится только две переменные, в которых мы будем хранить значения потенциального преобладающего элемента и кол-во раз, которое он встречается (счетчик).

Итак, сам алгоритм.

Проходим по каждому элементу заданной последовательности, проверяем значение счетчика. Если счетчик равен 0, то текущий элемент становится потенциальным кандидатом на преобладающий элемент (далее candidate), а счетчик увеличивается на 1. Далее если candidate равен текущему элементу, счетчик увеличивается, а если нет, то уменьшается.

Если мы не уверены, что в последовательности существует такой элемент, то нужно пройти по ней еще раз и убедится, что элемент встречается в массиве более чем n/2, где n - длина массива. P.S. В задаче с leetcode это не нужно, т.к. нам гарантируют, что такой элемент точно есть.

Почему вообще работает алгоритм и почему такой элемент всего один? Если бы существовали два различных элемента, которые каждый встречался бы более чем ⌊n/2⌋ раз, то их суммарное количество появлений превысило бы n, что невозможно.

Итак код:

public static int majorityElement(int[] nums) {
  //Часть 1 - Находим «кандидата на большинство»
  int candidate = nums[0];
  int count = 1;

  for (int i = 1; i < nums.length; i++) {
      if (count == 0) {
          candidate = nums[i];
      }
      count += (nums[i] == candidate) ? 1 : -1;
  }

  //Часть №2 - Проверяем, является ли кандидат большинством
  //не нужна для leetcode
  count = 0;
  for (int num : nums) {
      if (num == candidate) {
          count++;
      }
  }

  if (count > nums.length / 2) {
      return candidate;
  } else {
      throw new IllegalArgumentException("No majority element found");
  }
}

Вывод: алгоритм позволяет сократить пространственную сложность до О(1), что полезно, надо сказать:)

Комментарии (20)


  1. wataru
    05.08.2024 13:29
    +1

    У вас задачи перепутаны.

    Кратко: на вход мы получаем массив, состоящий из чисел. Нужно найти число, которое встречается наибольшее количество раз. 

    Не супер очевидно, но это число занимает больше половины элементов массива, т.е.

    Не очевидно, потому что это ни откуда не следует. В указанной вами задаче https://leetcode.com/problems/most-frequent-even-element/description/ такого перобладающего элемента вообще нет. Есть другая задача, где условие про "больше половины" задано в задаче: https://leetcode.com/problems/majority-element/description/


    1. akozyrenko Автор
      05.08.2024 13:29

      В ссылке, действительно, была ошибка. Скорректировала.


      1. wataru
        05.08.2024 13:29

        Тогда можно убрать условие про "встречается больше всех раз". В задаче этого условия нет. Потому что если элемент встречается больше половины раз, то он точно встречается больше всех остальных (даже вместе взятых, а уж по отдельности точно).


        1. akozyrenko Автор
          05.08.2024 13:29

          Согласна, скорректировала.


  1. wataru
    05.08.2024 13:29
    +2

    И еще, вы бы хоть показали, почему этот алгоритм вообще работает.


  1. sci_nov
    05.08.2024 13:29

    Получается, алгоритм находит моду дискретной случайной величины?


    1. wataru
      05.08.2024 13:29

      Только если она сильно доминирует - встречается хотябы половину раз во входном потоке. Ну и вообще, если величина случайная, то мода с ненулевой вероятностью может выпасть меньшее количество раз чем соседнее значение.


      1. sci_nov
        05.08.2024 13:29

        А что значит соседнее значение?


        1. wataru
          05.08.2024 13:29

          Ну, если гистограмма "хорошая", то соседние значения имеют почти такую-же высокую вероятность выпадения, каки мода. Но чуть поменьше. Но в общем случае, там не соседнее значение будет идти вторым по вероятности и можно получить случайно его.


          1. sci_nov
            05.08.2024 13:29

            А, понял, то есть данный алгоритм намного более простой, чем алгоритм нахождения моды, потому что мод может быть несколько.


            1. wataru
              05.08.2024 13:29

              Нет, он проще потому что тут не просто "мода", а сильно переобладающий элемент - он должен встречатся строго более половины раз.


              1. sci_nov
                05.08.2024 13:29

                Да, ПЭ - мода, но не всякая мода - ПЭ)


  1. DmitryZlobec
    05.08.2024 13:29

    if (count == 0) { candidate = nums[i]; }

    Я бы сохранил индекс в массиве где лежит элемент.


  1. homesoft
    05.08.2024 13:29

    Для последовательности 1,2,3,1,4,5,1,6,7,1,8,9 должно находить 1 ?


    1. akozyrenko Автор
      05.08.2024 13:29

      Нет, задача алгоритма (и, собственно, условие самой задачи на leetcode) найти число, которое встречается больше, чем n/2 раз, где n - длина входного массива.


      1. homesoft
        05.08.2024 13:29
        +1

        Просто в статье сначала идёт речь о "преобладающем элементе" (который будет в случае моего примера 1), потом о "преобладающем элементе, встречающемся более n/2 раз". Это может вводить в заблуждение..


        1. akozyrenko Автор
          05.08.2024 13:29
          +1

          Скорректировала:) надеюсь, теперь не будет путаницы

          Ну, и, кстати в статье уже был пример, похожий на ваш, где я написала, что в таком случае нет преобладающего элемента.


  1. Alexandroppolus
    05.08.2024 13:29

    Почему вообще работает алгоритм и почему такой элемент всего один? Если бы существовали два различных элемента, которые каждый встречался бы более чем ⌊n/2⌋ раз, то их суммарное количество появлений превысило бы n, что невозможно.

    На самом деле алгоритм обобщается на k элементов, каждый из которых встречается более чем ⌊n/(k+1)⌋ раз. Но это k надо знать заранее и сделать k счетчиков. Далее, смотрим очередной v = a[i]. Если этот v равен элементу на одном из занятых счетчиков, то увеличиваем данный счетчик, иначе если есть пустой счетчик, v его занимает, иначе все k счетчиков уменьшаются на 1. По итогу на счетчиках останутся искомые числа.


  1. HubForCyc1e
    05.08.2024 13:29

    Было бы здорово посмотреть видео объяснение, для новичков так легче приходит понимание, а за статьи отдельное спасибо!))