Трюк с XOR для собеседований и не только / Хабр

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

Хоть и непривычно ожидать решения с XOR на собеседованиях, довольно забавно разбираться, как они работают. Оказывается, все они основаны на одном фундаментальном трюке, который я постепенно раскрою в этом посте. Далее мы рассмотрим множество способов применения этого трюка с XOR, например, при решении популярной задачи с собеседований:

Дан массив из n — 1 целых чисел, находящихся в интервале от 1 до n. Все числа встречаются только один раз, за исключением одного числа, которого нет. Найдите отсутствующее число.

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

= value return result

С первого взгляда на код алгоритм понять сложно. Однако если знать, как работает трюк с XOR, то он становится довольно тривиальным. По-моему, именно поэтому не стоит ждать такого решения на собеседованиях: оно требует знания очень специфичного трюка, но почти никакого алгоритмического мышления.

Прежде чем мы перейдём к следующему способу применения, я сделаю пару замечаний.

Использование этого трюка не только для целых чисел

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

Можно придумать способы применения, где элементы не являются целыми числами от 1 до n:

  • Множество потенциальных элементов — это объекты Person и нам нужно найти Person, отсутствующего в списке значений
  • Множество потенциальных элементов — все узлы графа, и мы ищем отсутствующий узел
  • Множество потенциальных элементов — просто целые числа (необязательно с 1 до n) и нам нужно найти отсутствующее целое число

Арифметические операции вместо XOR

Если алгоритм по-прежнему кажется вам непостижимым и магическим (надеюсь, это не так), то может быть полезным подумать, как достичь того же результата при помощи арифметических операторов. На самом деле всё довольно просто:

def find_missing(A, n):
  result = 0
  # Add all the values from 1 to n
  for value in range(1, n + 1):
    result += value
  # Subtract all values in the given array
  for value in A:
    result -= value
  return result

Мы складываем все потенциальные целые числа, а затем вычитаем те, которые встречаются на самом деле. Это решение не такое красивое, потому что придётся обрабатывать переполнения, а также потому что тип элементов должен поддерживать

+, - с определёнными свойствами. Однако здесь используется та же логика взаимного уничтожения элементов, потому что они встречаются определённое количество раз (один раз как положительное, другой — как отрицательное).

Способ применения 3: поиск повторяющегося числа

И вот здесь всё становится интереснее: мы можем применить точно такое же решение к похожей задаче с собеседования:

Дан массив A из n + 1 целых чисел, находящихся в интервале от 1 до n.
Все числа встречаются ровно один раз, за исключением одного числа, которое повторяется. Найти это повторяющееся число.

Давайте подумаем, что произойдёт, если мы просто применим алгоритм из предыдущего решения. Мы получим последовательность операторов XOR, в которой элементы встречаются следующим образом:

  • Повторяющееся значение встречается три раза:
    • один раз из-за взятия всех значений от 1 до n
    • дважды, потому что оно повторяется в исходном массиве

  • Все остальные значения встречаются дважды:
    • один раз из-за взятия всех значений от 1 до n
    • один раз, потому что они были уникальными в исходном массиве

Как и ранее, все повторяющиеся элементы взаимно уничтожаются. Это означает, что у нас осталось именно то, что мы ищем: элемент, повторяющийся в исходном массиве. 0 = x

Все остальные элементы взаимно уничтожаются, потому что встречаются ровно два раза.

Способ применения 4: поиск двух отсутствующих/повторяющихся чисел

Оказывается, мы можем расширить возможности алгоритма. Рассмотрим чуть более сложную задачу:

Дан массив A из n — 2 целых чисел, находящихся в интервале от 1 до n. Все числа встречаются ровно один раз, за исключением двух отсутствующих чисел. Найти эти два отсутствующих числа.

Как и ранее, задача полностью эквивалентна поиску двух повторяющихся чисел.

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

v, т.е. первую позицию i, в которой u и v должны различаться. Затем мы разделим A, а также числа от 1 до n в соответствии с этим битом. В результате мы получим два сегмента, каждый из которых содержит два множества:

  1. Сегмент 0
    1. Множество всех значений от 1 до n, в которых
      i
      -тый бит равен 0
    2. Множество всех значений из A, в которых i-тый бит равен 0

  2. Сегмент 1
    1. Множество всех значений от 1 до n, в которых i-тый бит равен 1
    2. Множество всех значений из A, в которых i-тый бит равен 1

Так как u и v различаются в позиции i, то мы знаем, что они должны быть в разных сегментах.

Упрощаем задачу

Далее мы можем использовать ещё одно сделанное ранее открытие:

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

Эти два множества точно соответствуют множествам, находящимся в каждом из сегментов. Следовательно, мы можем выполнить поиск u, применив этот принцип к одному из сегментов и найдя отсутствующий элемент, а затем найти

v, применив его к другому сегменту.

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

Достигнуть предела

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

Следовательно, задача требует более сложных решений, больше не использующих XOR.

Заключительные мысли

Как говорилось выше, наверно, не стоит давать такие задачи на собеседованиях. Для их решения нужно знать не сразу понятный трюк, но если он известен, то решать больше практически нечего (возможно, за исключением способа применения 4). Едва ли таким образом кандидат продемонстрирует алгоритмическое мышление (кроме навыков упрощения) и здесь не особо получится использовать структуры данных.

Однако здорово было выяснить, как этот трюк работает. Похоже, XOR обладает идеально подходящими для этой задачи свойствами. Кроме того, есть некая красота в том, что нечто столь фундаментальное, как XOR, можно использовать для создания описанных в статье алгоритмов.



На правах рекламы

VDSina предлагает виртуальные серверы на Linux и Windows — выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.

Что такое BooleanUtils.xor в Java?

Надежные ответы на вопросы разработчиков

abhilash

Бесплатный курс собеседования по проектированию систем

Многие кандидаты отклоняются или понижаются в рейтинге из-за плохой успеваемости на собеседовании по проектированию систем. Выделитесь на собеседованиях по проектированию систем и получите работу в 2023 году благодаря этому популярному бесплатному курсу.

Обзор

xor() — это статический метод класса BooleanUtils , который принимает массив логических значений и выполняет логическое XOR операция над переданными значениями.

Как импортировать

BooleanUtils

Определение BooleanUtils можно найти в пакете Apache Commons Lang. Мы можем добавить это в проект Maven, добавив следующую зависимость в файл pom. xml :

 
            org.apache.commons
            commons-lang3
            <версия>3.12.0

 

Примечание: Другие версии пакета commons-lang см. в репозитории Maven.

Мы можем импортировать класс BooleanUtils следующим образом:


 import org.apache.commons.lang3.BooleanUtils;
 

Синтаксис

 public static boolean xor(final boolean... array)
 

Примечание: См. Что такое три точки в Java? чтобы понять ... .

Параметры

  • final boolean... array : Это список логических значений.

Возвращаемое значение

xor возвращает логическую операцию XOR для всех переданных логических значений.

Код

  0" encoding="UTF-8"?>
<проект xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-экземпляр"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    4.0.0
    org.example
    тест
    <версия>1.0-SNAPSHOT
    <свойства>
        8
        8
    
    <зависимости>
        <зависимость>
            org.apache.commons
            commons-lang3
            <версия>3.12.0
        
    
    <сборка>
        <плагины>
            <плагин>
                org.apache.maven.plugins
                плагин maven-shade
                <версия>2.1
                <выполнения>
                    <исполнение>
                        <фаза>пакет
                        <цели>
                            оттенок
                        
                        <конфигурация>
                            <трансформеры>
                                <трансформер
                                        реализация = "org. apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    Основной
                                
                            
                        
                    
                
            
        
    
 

Объяснение

Зависимость maven для BooleanUtils включена в файл pom.xml .

Main.java

  • Строки 1-2: мы импортируем класс BooleanUtils .
  • Строка 7: мы создаем массив логических значений с именем логических значений .
  • Строка 8: операция XOR применяется к логическим значениям массива boolean с использованием BooleanUtils.xor() 9) не учитывается при когнитивной сложности — IntelliJ Platform

    Cargonat (Тобиас Думмшат) 1

    • Плагин SonarLint в IntelliJ (версия плагина 4.10.0.19739)
    • Напишите метод с достаточно высокой когнитивной сложностью, чтобы вызвать проблему
    • Для меня следующий пример делает трюк с 20 когнитивной сложностью 9'не выделено из-за сложности с = "исключающее"; if (a && b) // '&&' выделено для сложности с = "и"; если (а || б)// '||' выделены для сложности с = "или"; // быстро увеличить когнитивную сложность с помощью вложенных операторов if если правда){ если правда){ если правда){ если правда){ если правда){ } } } } } возврат с; }

      alban.auzeill (Альбан Озейл)

      Использование здесь оператора != создает вложенные равенства, которые кажутся мне более сложными. Я также не считаю != логическим оператором.

      Что касается когнитивной сложности, я чувствую, что любая логическая операция немного увеличивает ее.

      С наилучшими пожеланиями
      Тобиас

      alban.auzeill (Альбан Озейл) 4 9б | c & d) { // Несоответствие \o/ вернуть 42; } вернуть 0; }

      Каргонат (Тобиас Думмшат) 5

      Привет, Албан

      Я не думаю, что расширение RSPEC-2178 — правильное решение. Это правило касается короткого замыкания, которое здесь не применяется.

      Похоже, что использование не дает технических преимуществ != 9 . Единственная разница, по-видимому, заключается в личных предпочтениях и личной легкости понимания, как видно из комментариев к ответам здесь.

      Лично я бы изменил когнитивную сложность, включив все три булевых логических оператора и, возможно, даже != при сравнении булевых операторов, поскольку эквивалентные варианты следует рассматривать одинаково.

      С наилучшими пожеланиями,
      Тобиас

      alban.auzeill (Альбан Озейл) было плохой идеей. Жаль, это было бы однократное изменение.

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

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

      • +1 для и и | с использованием той же логики, что и && 9 и !=

      Каргонат (Тобиас Думмшат)