Длинная арифметика

В этом уроке ты
  • Решишь задачу с собеседования в BigTech
  • Научишься решать задачу, которая не попадает под основные паттерны решения, от чего и может вызывать трудности.
Ты на собеседовании

Представь, что ты на собеседовании. У тебя есть 20 минут, чтобы придумать решение задачи!

Условие

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

Даны два непустых массива nums1 и nums2, где каждый элемент — это цифра (от 0 до 9). Эти массивы представляют собой длинные числа. Твоя задача — написать логику их сложения. Учитывай, что nums1 и nums2 могут быть очень длинными.

Пример 1

Ввод: nums1 = [1,2,3], nums2 = [4,5,6]
Вывод: [5,7,9]
Объяснение: 123 + 456 = 579

Пример 2

Ввод: nums1 = [9,9,9], nums2 = [9]
Вывод: [1,0,0,8]
Объяснение: 999 + 9 = 1008
В чем сложность задачи?

Главная сложность — найти подход к задаче и придумать лаконичное решение.

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

using System;
using System.Collections.Generic;

public class Solution
{
    // Время: O(max(n,m)), где n - размер nums1, m - размер nums2
    // Память: O(max(n,m))
    public static List<int> Calculate(List<int> nums1, List<int> nums2)
    {
        // Выравниваем длины массивов, заполняя ведущими нулями
        int maxLen = Math.Max(nums1.Count, nums2.Count);
        nums1.InsertRange(0, new int[maxLen - nums1.Count]);
        nums2.InsertRange(0, new int[maxLen - nums2.Count]);

        // Инициализация переменных для сложения
        int carry = 0;
        List<int> result = new List<int>();

        // Проходимся по числам справа налево
        for (int i = maxLen - 1; i >= 0; --i)
        {
            int digitSum = nums1[i] + nums2[i] + carry;
            carry = digitSum / 10;  // Сохраняем перенос
            result.Add(digitSum % 10);  // Оставляем только текущую цифру
        }

        // Если после последнего сложения остался перенос, добавляем его
        if (carry > 0)
        {
            result.Add(carry);
        }

        // Результат в обратном порядке, разворачиваем его обратно
        result.Reverse();
        return result;
    }
}

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

Оптимальное решение

Рассмотри, оптимальное решение (в комментариях довольно подробно расписано что, зачем и почему).

using System;
using System.Collections.Generic;

public class Solution
{
    public static List<int> Calculate(List<int> nums1, List<int> nums2)
    {
        List<int> result = new List<int>();
        // carry - число, которое переносим в следующий разряд
        int carry = 0;

        // складываем числа с конца, поэтому указатели ставим на конец
        // и двигаем в начало строки
        int p1 = nums1.Count - 1;
        int p2 = nums2.Count - 1;

        while (p1 >= 0 || p2 >= 0 || carry > 0)
        {
            // если выходим за границу массива, то текущее число
            // для сложения это 0, иначе достаем значение из массива
            int num1 = (p1 >= 0) ? nums1[p1] : 0;
            int num2 = (p2 >= 0) ? nums2[p2] : 0;

            // вычисляем сумму двух чисел и carry
            int overallSum = num1 + num2 + carry;
            // если overallSum = 17, то carry = 1,
            // а 7 должна быть записана в текущий разряд ответа
            carry = overallSum / 10;
            result.Add(overallSum % 10);

            // сдвигаем указатели к следующему разряду
            p1--;
            p2--;
        }

        // результат получился в обратном порядке, поэтому нужно развернуть
        result.Reverse();
        return result;
    }
}

Основная идея:

  1. Входящие массивы обходим с конца.
  2. Результат собираем последовательно, не забывая переносить разряд.
  3. Если какой-то из курсоров вышел за пределы, берём 0.
  4. Цикл повторяем до тех пор, пока не исчерпали оба массива и есть что переносить.
  5. Результат получается в обратном порядке, поэтому нужно его развернуть.
Оценка по времени и памяти
  • Время: O(max(n,m)), где n - размер nums1, m - размер nums2
  • Память: O(max(n,m))

Тут всё просто, двигаемся пока не пройдем полностью массивы, т.е пока не дойдем до конца самого длинного. Если строго, то max(n,m)+1 , но в Big-O опускаем единицу. И память также O(max(n,m)), поскольку выделяется под результирующий массив.

Что дальше

Вперед к задачкам! Решать и решать. Только через практику можно прийти к успеху.