Алгоритмы - Строки

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

Алгоритм решения

  1. Сначала проверяется, равны ли длины строк. Если нет, то они не могут быть анаграммами.
  2. Создаются два словаря (map) для подсчета частоты встречаемости каждого символа в обеих строках.
  3. Проходим по первой строке и заполняем первый словарь.
  4. Проходим по второй строке и заполняем второй словарь.
  5. Сравниваем частоты символов в обоих словарях. Если для какого-то символа частоты не совпадают, строки не являются анаграммами.
  6. Если все проверки пройдены успешно, строки являются анаграммами.

Временная сложность: O(n), где n - длина строк. Это происходит потому, что мы проходим по каждой строке один раз для заполнения словарей, а затем еще раз проходим по словарю для сравнения.

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

func isAnagram(s1, s2 string) bool {
    if len(s1) != len(s2) {
    return false
    }

    s1Rune := make(map[rune]int)
    s2Rune := make(map[rune]int)

    for _, r := range s1 {
    s1Rune[r]++
    }

    for _, r := range s2 {
    s2Rune[r]++
    }

    for r, count := range s1Rune {
    if count != s2Rune[r] {
    return false
    }
    }

    return true
}
                                        

Этот код реализует функцию strStr, которая решает задачу нахождения индекса первого вхождения подстроки (needle) в строку (haystack). Это аналог функции indexOf в некоторых языках программирования.

Алгоритм решения

  1. Проверяем краевые случаи:
    • Если needle пустая, возвращаем 0 (пустая строка всегда считается найденной в начале любой строки).
    • Если haystack короче needle, возвращаем -1 (невозможно найти).
  2. Проходим по строке haystack, используя скользящее окно размером с needle:
    • Для каждой позиции i в haystack проверяем, совпадает ли подстрока haystack[i:i+len(needle)] с needle.
    • Если совпадение найдено, возвращаем индекс i.
  3. Если после прохода по всей строке совпадение не найдено, возвращаем -1.

Временная сложность: O(n * m), где n - длина haystack, m - длина needle.

Пространственная сложность: O(1), так как используется только фиксированное количество дополнительных переменных, независимо от размера входных строк.

func strStr(haystack string, needle string) int {
	if len(needle) == 0 {
		return 0
	}

	if len(haystack) < len(needle) {
		return -1
	}

	for i := 0; i <= len(haystack)-len(needle); i++ {
		if haystack[i:i+len(needle)] == needle {
			return i
		}
	}

	return -1
}
                                        

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

Алгоритм решения

  1. Функция generateScobs инициализирует пустой слайс для результатов и вызывает вспомогательную рекурсивную функцию.
  2. Функция generateHelper использует рекурсию с бэктрекингом:
    • Базовый случай: если все скобки использованы, добавляем текущую последовательность в результат.
    • Рекурсивные случаи:
      • Если есть неиспользованные открывающие скобки, добавляем '(' и рекурсивно вызываем функцию.
      • Если закрывающих скобок осталось больше, чем открывающих, добавляем ')' и рекурсивно вызываем функцию.
  • Временная сложность: O(4^n / √n)
    • Это приближение n-го числа Каталана, которое определяет количество правильных скобочных последовательностей.
  • Пространственная сложность: O(n)
    • Обусловлена глубиной рекурсии и длиной хранимых строк.
func generateScobs(n int) []string {
	result := make([]string, 0)
	generateHelper(n, n, n, &result, "")
	return result
}

func generateHelper(n, left, right int, result *[]string, current string) {
	if left == 0 && right == 0 && current != "" {
		*result = append(*result, current)
		return
	}

	if left > 0 {
		generateHelper(n, left-1, right, result, current+"(")
	}

	if right > left {
		generateHelper(n, left, right-1, result, current+")")
	}
}
                                        

Этот код реализует функцию isSubsequence, которая проверяет, является ли строка s подпоследовательностью строки t. Подпоследовательность - это последовательность символов, которая появляется в той же последовательности в другой строке, но не обязательно непрерывно.

Алгоритм решения

  1. Проверка краевых случаев:
    • Если s пустая или равна t, возвращаем true.
    • Если s длиннее t, или они равной длины, но не равны, или t пустая, возвращаем false.
  2. Преобразование строк в слайсы рун для корректной обработки Unicode-символов.
  3. Итерация по символам строки t:
    • Если текущий символ совпадает с символом в s на позиции курсора, увеличиваем курсор.
    • Если курсор достиг конца s, возвращаем true (вся подпоследовательность найдена).
  4. Если после прохода по всей строке t не все символы s найдены, возвращаем false.
  • Временная сложность: O(t), где t - длина строки t
    • В худшем случае алгоритм проходит по всей строке t один раз.
  • Пространственная сложность: O(m+n), где m и n - длины строк s и t соответственно
    • Создаются два новых слайса рун для хранения символов обеих строк.
func isSubsequence(s string, t string) bool {
	if s == t || s == "" {
		return true
	}
	if len(s) > len(t) || t == "" {
		return false
	}

	rS := []rune(s)
	rT := []rune(t)

	cursor := 0
	for _, c := range rT {
		if rS[cursor] == c {
			cursor++
		}

		if cursor == len(rS) {
			return true
		}
	}

	return false
}
                                        

Функция checkCorrectScobs проверяет корректность строки, содержащей только круглые скобки, и возвращает количество правильно закрытых пар скобок и булево значение, указывающее на общую корректность последовательности.

Алгоритм решения

  1. Инициализация переменных:
    • isCorrect: флаг корректности последовательности
    • count: счетчик правильно закрытых пар скобок
    • countOpen: счетчик открытых скобок
  2. Итерация по строке:
    • Если встречается открывающая скобка '(', увеличиваем countOpen
    • Если встречается закрывающая скобка ')':
      • Проверяем, есть ли открытые скобки (countOpen > 0)
      • Если нет, возвращаем (0, false)
      • Если есть, уменьшаем countOpen и увеличиваем count
  3. После итерации:
    • Если остались незакрытые скобки (countOpen != 0), устанавливаем isCorrect = false
  4. Возвращаем count и isCorrect
  • Временная сложность: O(n)
    • Где n - длина входной строки
    • Алгоритм проходит по строке ровно один раз
  • Пространственная сложность: O(1)
    • Используется постоянное количество дополнительной памяти, независимо от размера входной строки
func checkCorrectScobs(str string) (int, bool) {
	isCorrect := true
	count := 0
	countOpen := 0

	for i := 0; i < len(str); i++ {
		if str[i] == '(' {
			countOpen++
			continue
		}

		if str[i] == ')' {
			if countOpen == 0 {
				return 0, false
			}
			countOpen--
			count++
		}
	}

	if countOpen != 0 {
		isCorrect = false
	}

	return count, isCorrect
}
                                        

Функция isPolindrom проверяет, являются ли две входные строки палиндромами друг друга. То есть, является ли вторая строка обратной первой.

Алгоритм решения

  1. Проверка длины строк:
    • Если длины строк не равны, возвращаем false
  2. Преобразование строк в слайсы рун:
    • Это позволяет корректно работать с Unicode-символами
  3. Итерация по символам:
    • Сравниваем символы с начала первой строки с символами с конца второй строки
    • Если находим несоответствие, возвращаем false
  4. Если все символы совпали, возвращаем true
  • Временная сложность: O(n)
    • Где n - длина строки
    • Алгоритм проходит по половине символов строки
  • Пространственная сложность: O(n)
    • Создаются два новых слайса рун, каждый длиной n
func isPolindrom(s1, s2 string) bool {
	if len(s1) != len(s2) {
		return false
	}

	s1Rune := []rune(s1)
	s2Rune := []rune(s2)

	for i := 0; i <= len(s1Rune)/2+1; i++ {
		if s1Rune[i] != s2Rune[len(s2Rune)-i-1] {
			return false
		}
	}

	return true
}
                                        

Функция isPalindrome проверяет, является ли заданная строка палиндромом, игнорируя регистр букв и не учитывая не буквенно-цифровые символы.

Алгоритм решения

  1. Инициализация указателей:
    • Устанавливаем левый указатель на начало строки, правый - на конец
  2. Итерация по строке:
    • Сравниваем символы с левого и правого концов строки
    • Приводим символы к нижнему регистру
    • Пропускаем не буквенно-цифровые символы
    • Если символы не совпадают, возвращаем false
    • Сдвигаем указатели к центру строки
  3. Проверка завершена:
    • Если все символы совпали, возвращаем true
  • Временная сложность: O(n)
    • Где n - длина входной строки
    • Алгоритм проходит по строке только один раз
  • Пространственная сложность: O(1)
    • Используется постоянное количество дополнительной памяти, независимо от размера входной строки
func isPalindrome(s string) bool {
	left, right := 0, len(s)-1

	for left < right {
		leftChar := unicode.ToLower(rune(s[left]))
		rightChar := unicode.ToLower(rune(s[right]))

		if !isAlphanumeric(leftChar) {
			left++
			continue
		}
		if !isAlphanumeric(rightChar) {
			right--
			continue
		}

		if leftChar != rightChar {
			return false
		}

		left++
		right--
	}

	return true
}

func isAlphanumeric(r rune) bool {
	return unicode.IsLetter(r) || unicode.IsDigit(r)
}
                                        

Функция isValid проверяет, является ли входная строка корректным выражением с правильно расставленными скобками трех типов: круглыми (), квадратными [] и фигурными {}.

Алгоритм решения

  1. Инициализация:
    • Создаем пустой стек для хранения открывающих скобок
    • Определяем map для сопоставления закрывающих скобок с открывающими
  2. Итерация по строке:
    • Если символ - открывающая скобка, добавляем его в стек
    • Если символ - закрывающая скобка:
      • Проверяем, пуст ли стек или не соответствует ли последняя открывающая скобка в стеке
      • Если условие выполняется, возвращаем false
      • Иначе удаляем последнюю открывающую скобку из стека
  3. Проверка завершена:
    • Возвращаем true, если стек пуст (все скобки закрыты)
  • Временная сложность: O(n)
    • Где n - длина входной строки
    • Алгоритм проходит по строке только один раз
  • Пространственная сложность: O(n)
    • В худшем случае (когда все символы - открывающие скобки) стек может содержать все n символов
func isValid(s string) bool {
	stack := make([]rune, 0)
	parentheses := map[rune]rune{')': '(', ']': '[', '}': '{'}

	for _, c := range s {
		if c == '(' || c == '[' || c == '{' {
			stack = append(stack, c)
		} else if len(stack) == 0 || parentheses[c] != stack[len(stack)-1] {
			return false
		} else {
			stack = stack[:len(stack)-1]
		}
	}

	return len(stack) == 0
}
                                        

Функция longestPalindrome находит самый длинный палиндром в заданной строке.

Алгоритм решения

  1. Проверка граничных случаев:
    • Если длина строки меньше 2, возвращаем саму строку
  2. Итерация по каждому символу строки:
    • Для каждого символа проверяем палиндромы с нечетной длиной (центр - текущий символ)
    • Для каждого символа проверяем палиндромы с четной длиной (центр - между текущим и следующим символом)
    • Используем вспомогательную функцию expandAroundCenter для расширения палиндрома
    • Обновляем информацию о самом длинном найденном палиндроме
  3. Возвращаем найденный самый длинный палиндром
  • Временная сложность: O(n^2)
    • n - длина входной строки
    • Для каждого символа мы можем расширяться до краев строки
  • Пространственная сложность: O(1)
    • Используется постоянное количество дополнительной памяти
func longestPalindrome(s string) string {
	if len(s) < 2 {
		return s
	}

	start, maxLength := 0, 1

	for i := 0; i < len(s); i++ {
		// Проверяем палиндромы с нечетной длиной
		len1 := expandAroundCenter(s, i, i)
		// Проверяем палиндромы с четной длиной
		len2 := expandAroundCenter(s, i, i+1)

		length := max(len1, len2)
		if length > maxLength {
			start = i - (length-1)/2
			maxLength = length
		}
	}

	return s[start : start+maxLength]
}

func expandAroundCenter(s string, left, right int) int {
	for left >= 0 && right < len(s) && s[left] == s[right] {
		left--
		right++
	}
	return right - left - 1
}
                                        

Функция lengthOfLongestSubstring находит длину самой длинной подстроки без повторяющихся символов в заданной строке.

Алгоритм решения

  1. Проверка на пустую строку
  2. Преобразование строки в массив рун (для корректной работы с Unicode)
  3. Инициализация переменных:
    • subLengs - длина самой длинной найденной подстроки
    • curSubRunes - карта для хранения последних позиций символов
  4. Использование техники "скользящего окна":
    • Проход по строке с двумя указателями: left и right
    • Если символ уже встречался в текущем окне, сдвигаем левую границу
    • Обновление позиции текущего символа в карте
    • Обновление максимальной длины подстроки, если текущее окно больше
  5. Возврат длины самой длинной подстроки
  • Временная сложность: O(n)
    • n - длина входной строки
    • Алгоритм проходит по строке один раз
  • Пространственная сложность: O(k)
    • k - размер алфавита (максимальное количество уникальных символов)
    • В худшем случае может быть O(min(n, m)), где m - размер используемого алфавита
func lengthOfLongestSubstring(s string) int {
	if s == "" {
		return 0
	}

	runes := []rune(s)
	left, subLengs := 0, 0
	curSubRunes := make(map[rune]int)

	for right := 0; right < len(runes); right++ {
		// Если текущий символ уже встречался в текущем окне, сдвигаем левую границу окна.
		if idx, ok := curSubRunes[runes[right]]; ok && idx >= left {
			left = idx + 1
		}
		curSubRunes[runes[right]] = right // пишем текущий символ
		// если текущее окно больше предыдущего обновляем
		subLengs = max(subLengs, right-left+1)
	}

	return subLengs
}
                                        

Дано две строки s1 и s2. Необходимо определить, содержит ли строка s2 перестановку строки s1 в качестве подстроки. Задача известна как "Permutation in String".

Алгоритм решения

  1. Проверяем, что длина s1 не превышает длину s2. Если превышает, возвращаем false.
  2. Создаем два массива count1 и count2 размером 256 для подсчета частоты символов.
  3. Заполняем count1 частотами символов из s1 и count2 частотами символов из первого окна s2 длиной len(s1).
  4. Проверяем, равны ли count1 и count2. Если да, возвращаем true.
  5. Используем метод скользящего окна для проверки остальной части s2:
    • Уменьшаем счетчик для символа, выходящего из окна.
    • Увеличиваем счетчик для нового символа, входящего в окно.
    • Сравниваем count1 и count2. Если равны, возвращаем true.
  6. Если ни одно окно не дало совпадения, возвращаем false.

Особенности решения

  • Работает с расширенным набором символов UTF-8, включая эмодзи и другие Unicode символы.
  • Эффективно использует метод скользящего окна для оптимизации производительности.
  • Не требует сортировки или преобразования строк, что улучшает эффективность.
  • Подходит для работы с большими строками благодаря линейной временной сложности.
  • Временная сложность: O(n), где n - длина строки s2. Мы проходим по s2 один раз.
  • Пространственная сложность: O(1), так как используются массивы фиксированного размера (256) независимо от входных данных.
func checkInclusion(s1 string, s2 string) bool {
    if len(s1) > len(s2) {
        return false
    }

    // Создаем слайсы для подсчета символов
    count1 := make([]int, 256)
    count2 := make([]int, 256)

    // Заполняем count1 для s1 и первого окна s2
    for i := 0; i < len(s1); i++ {
        count1[s1[i]]++
        count2[s2[i]]++
    }

    // Проверяем первое окно
    if isEqual(count1, count2) {
        return true
    }

    // Скользящее окно для остальной части s2
    for i := len(s1); i < len(s2); i++ {
        count2[s2[i-len(s1)]]--
        count2[s2[i]]++
        if isEqual(count1, count2) {
            return true
        }
    }

    return false
}

func isEqual(count1, count2 []int) bool {
    for i := 0; i < 256; i++ {
        if count1[i] != count2[i] {
            return false
        }
    }
    return true
}
                                        

Функция simplifyPath преобразует абсолютный путь в файловой системе в канонический путь, удаляя избыточные элементы и нормализуя структуру.

Алгоритм решения

  1. Разделение входного пути на компоненты с помощью strings.Split
  2. Инициализация стека для хранения валидных компонентов пути
  3. Итерация по компонентам пути:
    • Игнорирование пустых компонентов и "." (текущая директория)
    • Обработка ".." (переход на уровень выше) путем удаления последнего элемента из стека
    • Добавление валидных компонентов в стек
  4. Сборка результирующего пути из элементов стека
  • Временная сложность: O(n)
    • n - количество компонентов в пути
    • Один проход по всем компонентам пути
  • Пространственная сложность: O(n)
    • В худшем случае стек может содержать все компоненты пути
func simplifyPath(path string) string {
    // Разделяем путь на компоненты
    components := strings.Split(path, "/")
    stack := []string{}

    for _, component := range components {
        switch component {
        case "", ".":
            // Игнорируем пустые компоненты и текущую директорию
            continue
        case "..":
            // Переходим на уровень выше, если это возможно
            if len(stack) > 0 {
                stack = stack[:len(stack)-1]
            }
        default:
            // Добавляем валидный компонент пути
            stack = append(stack, component)
        }
    }

    // Собираем упрощенный путь
    result := "/" + strings.Join(stack, "/")
    return result
}
                                        

Функция compress выполняет сжатие строки, заменяя последовательности повторяющихся символов на символ и количество его повторений. Сжатие выполняется "на месте", модифицируя входной массив байтов.

Алгоритм решения

  1. Проверка на короткие входные данные (длина 0 или 1)
  2. Инициализация индекса записи и счетчика повторений
  3. Итерация по символам строки:
    • Подсчет последовательных повторений символа
    • При смене символа или достижении конца строки:
      • Запись символа
      • Запись количества повторений (если больше 1)
      • Обновление индекса записи
  4. Возврат новой длины сжатой строки
  • Временная сложность: O(n)
    • n - длина входной строки
    • Один проход по всем символам
  • Пространственная сложность: O(1)
    • Модификация выполняется "на месте"
    • Используется фиксированное количество дополнительных переменных
func compress(chars []byte) int {
    if len(chars) <= 1 {
        return len(chars)
    }

    writeIndex := 0
    count := 1

    for i := 1; i <= len(chars); i++ {
        if i < len(chars) && chars[i] == chars[i-1] {
            count++
        } else {
            chars[writeIndex] = chars[i-1]
            writeIndex++

            if count > 1 {
                countStr := strconv.Itoa(count)
                for _, digit := range countStr {
                    chars[writeIndex] = byte(digit)
                    writeIndex++
                }
            }
            count = 1
        }
    }

    return writeIndex
}
                                        

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

Пример:

Входная строка: AAABCCCCCDD

Результат: A3BC5D2

Функция должна возвращать ошибку, если на вход приходит недопустимая строка (содержащая символы, отличные от заглавных латинских букв).

Алгоритм решения

Алгоритм решения задачи состоит из следующих шагов:

  1. Проверяем, что строка не пуста. Если строка пуста, возвращаем пустую строку.
  2. Проверяем, что все символы в строке являются заглавными латинскими буквами. Если нет, возвращаем ошибку.
  3. Инициализируем переменные для хранения текущего символа, счетчика повторений и результирующей строки.
  4. Проходим по строке, начиная со второго символа:
    • Если текущий символ совпадает с предыдущим, увеличиваем счетчик.
    • Если символ отличается, добавляем предыдущий символ и его количество (если оно больше 1) в результат, затем обновляем текущий символ и сбрасываем счетчик.
  5. После завершения цикла добавляем последний символ и его количество (если оно больше 1) в результат.
  6. Возвращаем полученную сжатую строку.

Временная сложность: O(n), где n - длина входной строки.

Мы проходим по строке ровно один раз, выполняя константное количество операций для каждого символа.

Пространственная сложность: O(n) в худшем случае.

В худшем случае, когда все символы в строке различны (например, "ABCDEF"), результирующая строка будет иметь примерно такую же длину, как и входная. В лучшем случае (например, для строки с большим количеством повторений, как "AAAAAA"), пространственная сложность может быть значительно меньше O(n).

func CompressString(s string) (string, error) {
    if len(s) == 0 {
        return "", nil
    }

    // Проверка на допустимые символы (только заглавные латинские буквы)
    for _, char := range s {
        if !unicode.IsUpper(char) || !unicode.IsLetter(char) {
            return "", errors.New("строка содержит недопустимые символы")
        }
    }

    var result string
    count := 1
    currentChar := s[0]

    // Проходим по строке начиная со второго символа
    for i := 1; i < len(s); i++ {
        if s[i] == currentChar {
            // Если текущий символ совпадает с предыдущим, увеличиваем счетчик
            count++
        } else {
            // Если символ отличается, добавляем предыдущий символ и его количество в результат
            result += string(currentChar)
            if count > 1 {
                result += strconv.Itoa(count)
            }
            // Сбрасываем счетчик и обновляем текущий символ
            currentChar = s[i]
            count = 1
        }
    }

    // Добавляем последний символ и его количество
    result += string(currentChar)
    if count > 1 {
        result += strconv.Itoa(count)
    }

    return result, nil
}
                                        

Даны две строки s и t одинаковой длины. Необходимо определить, можно ли преобразовать строку s в строку t, выполнив не более одного циклического сдвига некоторого подотрезка строки s. Циклический сдвиг подотрезка [l, r] означает перемещение символов в этом подотрезке либо влево, либо вправо на одну позицию по кругу.

Алгоритм решения

  1. Проверить, что длины строк s и t одинаковы. Если нет, вернуть false.
  2. Использовать два указателя l и r для нахождения несовпадающего подотрезка строк s и t.
  3. Если все символы совпадают, вернуть true.
  4. Если l >= len(s)-r, то невозможно сделать циклический сдвиг, вернуть false.
  5. Проверить возможность циклического сдвига влево и вправо для несовпадающего подотрезка.
  6. Если ни один из сдвигов не приводит к совпадению, вернуть false.

Временная сложность: O(n), где n - длина строки. Алгоритм проходит по строкам только один раз.
Пространственная сложность: O(1), так как используется только несколько дополнительных переменных.

func canTransform(s, t string) bool {
    if len(s) != len(t) {
        return false
    }

    l, r := 0, len(s)-1

    // Находим первое несовпадение слева
    for l <= r && s[l] == t[l] {
        l++
    }
    // Находим первое несовпадение справа
    for l <= r && s[r] == t[r] {
        r--
    }

    // Если все символы совпадают, возвращаем true
    if l > r {
        return true
    }

    // Проверяем, можно ли сделать циклический сдвиг влево
    if canShiftLeft(s[l:r+1], t[l:r+1]) {
        return true
    }

    // Проверяем, можно ли сделать циклический сдвиг вправо
    return canShiftRight(s[l:r+1], t[l:r+1])
}

func canShiftLeft(s, t string) bool {
	for i := 0; i < len(s); i++ {
		// Выражение (i+1)%len(s) используется для циклического перехода в конец строки, когда i+1 становится равным длине строки.
		if s[i] != t[(i+1)%len(s)] {
			return false
		}
	}
	return true
}

func canShiftRight(s, t string) bool {
	for i := 0; i < len(s); i++ {
		// Выражение (i-1+len(s))%len(s) используется для:
		//1. Добавления len(s) к i-1, чтобы избежать отрицательных индексов.
		//2. Использования операции % для циклического перехода в конец строки.
		if s[i] != t[(i-1+len(s))%len(s)] {
			return false
		}
	}
	return true
}