Как написать функцию поиска, чтобы получить путь к нужному файлу?

У меня есть структура каталогов, которая выглядит как изображение. Как написать функцию поиска, чтобы получить путь к нужному файлу?

Эта структура организована в виде objects. File объекты имеют форму: {type:'file', name:'file a1'} например. Folderобъекты похожи на объекты File, но у них есть дополнительный ключ children, который представляет собой массив вложенных папок/файлов.

Вот консольный журнал объекта rootFolder с изображения до:

Как написать функцию поиска, чтобы получить путь к нужному файлу?

Цель: Я хочу написать функцию поиска, которая возвращает путь к определенному файлу в виде массива. Например, если бы я искал file c3, результат был бы: ['folder a3', 'folder b4'].

Я пробовал много вещей, но рекурсия не самая сильная моя сторона.

Код может быть в JavaScript или питон; меня волнует сам алгоритм.

Вот код, используемый для генерации rootFolder:

// defining constants
const TYPES = {FILE: 'file', FOLDER: 'folder'}
const FILE_NAMES = {
  FILE_A1:'file a1', 
  FILE_A2:'file a2', 
  FILE_A4:'file a4',
  FILE_B2:'file b2',
  FILE_B3:'file b3',
  FILE_C1:'file c1',
  FILE_C2:'file c2',
  FILE_C3:'file c3',
  FILE_C4:'file c4',
  FILE_C5:'file c5',
}
const FOLDER_NAMES = {
  ROOT_FOLDER:'root folder',
  FOLDER_A3:'folder a3',
  FOLDER_B1:'folder b1',
  FOLDER_B4:'folder b4'
}
// defining classes
class File {
  constructor(type, name) {
    this.name = name
    this.type = type
  }
}
class Folder {
  constructor (type, name, children) {
    this.name = name
    this.type = type
    this.children = children
  }
}
// defining file objects
const fileA1 = new File(TYPES.FILE, FILE_NAMES.FILE_A1)
const fileA2 = new File(TYPES.FILE, FILE_NAMES.FILE_A2)
const fileA4 = new File(TYPES.FILE, FILE_NAMES.FILE_A4)
const fileB2 = new File(TYPES.FILE, FILE_NAMES.FILE_B2)
const fileB3 = new File(TYPES.FILE, FILE_NAMES.FILE_B3)
const fileC1 = new File(TYPES.FILE, FILE_NAMES.FILE_C1)
const fileC2 = new File(TYPES.FILE, FILE_NAMES.FILE_C2)
const fileC3 = new File(TYPES.FILE, FILE_NAMES.FILE_C3)
const fileC4 = new File(TYPES.FILE, FILE_NAMES.FILE_C4)
const fileC5 = new File(TYPES.FILE, FILE_NAMES.FILE_C5)
// defining folder objects
const folderB1 = new Folder(TYPES.FOLDER, FOLDER_NAMES.FOLDER_B1, [fileC1, fileC2])
const folderB4 = new Folder(TYPES.FOLDER, FOLDER_NAMES.FOLDER_B4, [fileC3, fileC4, fileC5])
const folderA3 = new Folder(TYPES.FOLDER, FOLDER_NAMES.FOLDER_A3, [folderB1, fileB2, fileB3, folderB4])
const rootFolder = new Folder(TYPES.FOLDER, FOLDER_NAMES.ROOT_FOLDER, [fileA1, fileA2, folderA3, fileA4])

console.info(rootFolder);

Я не понимаю, как file c3 соединяется с folder a3 and folder b4. папка b4 является значением ключа «имя» в группе объектов file c3, но как насчет folder a3, как она связана с file c3? Может папка с массивом номер 3?

seriously 09.05.2022 03:40

@серьезно, я добавил код, используемый для генерации rootFolder. file c3 является ребенком folder b4 который является ребенком folder a3

Amrovic 09.05.2022 03:47

Stack Overflow не является сервисом для написания кода или обучения. Мы ожидаем, что вы сделаете честная попытка решения, опубликуете эту попытку, а затем зададите конкретные вопросы о ней (т. е. объясните, почему она не сработала или в чем проблема).

martineau 09.05.2022 04:04

Суть того, что вы хотите сделать, называется обход дерева — теперь вы знаете, где найти некоторые алгоритмы…

martineau 09.05.2022 04:09

Вы пробовали что-то вроде DFS/BFS? Должно быть самое простое решение имхо

Abhinav Mathur 09.05.2022 05:48
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
5
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

удалить направление

Прежде чем мы начнем, сначала удалите множество косвенность, вызванных более чем 20 переменными. Мы можем использовать только два (2) класса и один root -

class File {
  constructor(type, name) {
    this.name = name
    this.type = type
  }
}

class Folder {
  constructor(type, name, children) {
    this.name = name
    this.type = type
    this.children = children
  }
}

const root =
  new Folder("folder", "root folder", [
    new File("file", "file a1"),
    new File("file", "file a2"),
    new Folder("folder", "folder a3", [
      new Folder("folder", "folder b1", [
        new File("file", "file c1"),
        new File("file", "file c2")
      ]),
      new File("file", "file b2"),
      new File("file", "file b3"),
      new Folder("folder", "folder b4", [
        new File("file", "file c3"),
        new File("file", "file c4"),
        new File("file", "file c5")
      ])
    ]),
    new File("file", "file a4")
  ])

удалить избыточность

TYPES.FILE и TYPES.FOLDER не нужны, если у вас есть отдельные классы File и Folder. Каждый экземпляр уже будет иметь связанный тип —

class File {
  constructor(name) { // ✅ type not necessary
    this.name = name
  }
}

class Folder {
  constructor (name, children) { // ✅ type not necessary
    this.name = name
    this.children = children
  }
}

const root =
  new Folder("root folder", [
    new File("file a1"),
    new File("file a2"),
    new Folder("folder a3", [
      new Folder("folder b1", [
        new File("file c1"),
        new File("file c2")
      ]),
      new File("file b2"),
      new File("file b3"),
      new Folder("folder b4", [
        new File("file c3"),
        new File("file c4"),
        new File("file c5")
      ])
    ]),
    new File("file a4")
  ])

несколько возвращаемых значений

Поскольку файловые системы поддерживают папки или файлы с одинаковыми именами (в разных иерархиях), важно, чтобы наш алгоритм search мог сканировать все возможных совпадений запроса. Для полного примера давайте включим file c3 в folder b1иfolder b4. При поиске этого файла мы ожидаем, что будут возвращены оба пути —

const root =
  new Folder("root folder", [
    new File("file a1"),
    new File("file a2"),
    new Folder("folder a3", [
      new Folder("folder b1", [
        new File("file c1"),
        new File("file c2"),
        new File("file c3")  // ✅ could have the same name
      ]),
      new File("file b2"),
      new File("file b3"),
      new Folder("folder b4", [
        new File("file c3"), // ✅ could have the same name
        new File("file c4"),
        new File("file c5")
      ])
    ]),
    new File("file a4")
  ])

алгоритм

Теперь, когда мы исправили другие проблемы с кодом, давайте напишем search(t, query). Мы можем сделать это, используя индуктивное мышление

  1. Если t является файлом и его name соответствует query, выдать одноэлементный результат [t]
  2. (индуктивный) t не является файлом. Если t является папкой и ее name соответствует query, введите одноэлементный результат [t]. И для всех child из t.children, для всех path рекурсивной подзадачи search(child, query) добавьте t к path и уступите.
  3. (индуктивная) t не является ни файлом, ни папкой. Выдать ошибку типа, чтобы уведомить вызывающую сторону.
function* search(t, query) {
  switch(t?.constructor) {
    case File:
      if (t.name == query)
        yield [t]
      break
    case Folder:
      if (t.name == query)
        yield [t]
      for (const child of t.children)
        for (const path of search(child, query))
          yield [t, ...path]
      break
    default:
      throw Error(`cannot search unsupported type ${t}`)
  }
}

Вот несколько примеров поиска -

for (const path of search(root, "root folder"))
  console.info([".", ...path.map(f => f.name)].join("/"))
./root folder
for (const path of search(root, "file a1"))
  console.info([".", ...path.map(f => f.name)].join("/"))
./root folder/file a1

Несколько путей возвращаются, когда файл или папка найдены несколько раз -

for (const path of search(root, "file c3"))
  console.info([".", ...path.map(f => f.name)].join("/"))
./root folder/folder a3/folder b1/file c3
./root folder/folder a3/folder b4/file c3

Когда файл или папка не могут быть найдены, вывод не выдается -

for (const path of search(root, "file Q"))
  console.info([".", ...path.map(f => f.name)].join("/"))
⚠️ no output

вызывающий решает эффект

Получая последовательность объектов Folder и File, наш алгоритм search остается универсальным и пригодным для повторного использования. По умолчанию строки не собираются, и в консоль ничего не записывается. Если вызывающая сторона хочет создать путь, разделенный /, это продемонстрировано выше. Варианты другого эффекта оставляются на усмотрение вызывающего абонента.

только один результат

Использование генератора идеально подходит для search, потому что вызывающая сторона может использовать все совпадения или любое их количество. В этом примере мы используем общий first, чтобы вернуть только совпадение первый. Обратите внимание, что генератор отменяется сразу после того, как найдено совпадение, и процессор не выполняет никакой дополнительной работы.

function first(iter) {
  for (const v of iter)
    return v
}

const match = first(search(root, "file c3"))

if (match)
  console.info("found:", match.map(f => f.name).join("/"))
else
  console.error("no match found")
found: root folderfolder a3/folder b1/file c3

Убедитесь, что вы выполняете правильную проверку нуля, если используете first таким образом, поскольку возможно, что search ничего не даст, если совпадение не найдено -

const match = first(search(root, "ZZZ"))

if (match)
  console.info("found:", match.map(f => f.name).join("/"))
else
  console.error("no match found")
no match found

поиск высшего порядка

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

function* search(t, query) {
  switch(t?.constructor) {
    case File:
      if (Boolean(query(t.name))) // ✅ query is a func
        yield [t]
      break
    case Folder:
      if (Boolean(query(t.name))) // ✅ query is a func
        yield [t]
      for (const child of t.children)
        for (const path of search(child, query))
          yield [t, ...path]
      break
    default:
      throw Error(`cannot search unsupported type ${t}`)
  }
}

Найдем все файлы, которые начинаются с file c -

for (const path of search(root, filename => filename.startsWith("file c")))
  console.info([".", ...path.map(f => f.name)].join("/"))
./root folder/folder a3/folder b1/file c1
./root folder/folder a3/folder b1/file c2
./root folder/folder a3/folder b1/file c3
./root folder/folder a3/folder b4/file c3
./root folder/folder a3/folder b4/file c4
./root folder/folder a3/folder b4/file c5

специализация

С помощью функции высшего порядка search вы можете легко реализовать такие вещи, как searchByString. Это известно как специализация -

function searchByString(t, query) {
  return search(t, filename => filename == query)
}
for (const path of searchByString(root, "file c3"))
  console.info([".", ...path.map(f => f.name)].join("/"))
./root folder/folder a3/folder b1/file c3
./root folder/folder a3/folder b4/file c3

демонстрация кода

Приведенный выше код был сжат, поэтому вы можете увидеть его все в одном месте. Бежать фрагмент и проверьте вывод -

class File { constructor(name) { this.name = name } }

class Folder { constructor(name, children) { this.name = name;  this.children = children } }

function* search(t, query) {
  switch(t?.constructor) {
    case File: if (Boolean(query(t.name))) yield [t];  break
    case Folder: if (Boolean(query(t.name))) yield [t]; for (const child of t.children) for (const path of search(child, query)) yield [t, ...path]; break
    default: throw Error(`cannot search unsupported type ${t}`)
  }
}

function searchByString(t, query) {
  return search(t, filename => filename == query)
}

const relative = (path) => [".", ...path.map(f => f.name)].join("/")

const root = new Folder("root folder", [new File("file a1"), new File("file a2"), new Folder("folder a3", [new Folder("folder b1", [new File("file c1"), new File("file c2"), new File("file c3")]), new File("file b2"), new File("file b3"), new Folder("folder b4", [new File("file c3"), new File("file c4"), new File("file c5")])]), new File("file a4")])

console.info("== exact match = = ")
for (const path of search(root, filename => filename == "root folder"))
  console.info(relative(path))

console.info("== starts with 'file c' = = ")
for (const path of search(root, filename => filename.startsWith("file c")))
  console.info(relative(path))

console.info("== searchByString = = ")
for (const path of searchByString(root, "file c3"))
  console.info(relative(path))
.as-console-wrapper { min-height: 100%; top: 0; }
== exact match ==
./root folder
== starts with 'file c' ==
./root folder/folder a3/folder b1/file c1
./root folder/folder a3/folder b1/file c2
./root folder/folder a3/folder b1/file c3
./root folder/folder a3/folder b4/file c3
./root folder/folder a3/folder b4/file c4
./root folder/folder a3/folder b4/file c5
== searchByString ==
./root folder/folder a3/folder b1/file c3
./root folder/folder a3/folder b4/file c3

питон

search точно так же написано на Python -

class file:
  def __init__(self, name):
    self.name = name

class folder:
  def __init__(self, name, children):
    self.name = name
    self.children = children

def search(t, query):
  if isinstance(t, file):
    if t.name == query:
      yield [t]
  elif isinstance(t, folder):
    if t.name == query:
      yield [t]
    for child in t.children:
      for path in search(child, query):
        yield [t, *path]
  else:
    raise TypeError("unsupported", t)

Если вы используете Python 3.10, вам доступны некоторые новые функции. Настройте search, чтобы воспользоваться преимуществами нового структурного синтаксиса match..case -

from dataclasses import dataclass
from typing import Generator, Union

Ftree = Union["file", "folder"]

@dataclass
class file:
  name: str

@dataclass
class folder:
  name: str
  children: list[Ftree]

def search(t: Ftree, query: str) -> Generator[list[Ftree], None, None]:
  match t:
    case file(name):
      if name == query: yield [t]
    case folder(name, children):
      if name == query: yield [t]
      for child in t.children:
        for path in search(child, query):
          yield [t, *path]
    case _:
      raise TypeError("unsupported", t)

Дано дерево -

root = \
  folder("root folder", [
    file("file a1"),
    file("file a2"),
    folder("folder a3", [
      folder("folder b1", [
        file("file c1"),
        file("file c2"),
        file("file c3")
      ]),
      file("file b2"),
      file("file b3"),
      folder("folder b4", [
        file("file c3"),
        file("file c4"),
        file("file c5")
      ])
    ]),
    file("file a4")
  ])

Обе реализации ведут себя одинаково -

for path in search(root, "root folder"):
  print("/".join([".", *map(lambda f: f.name, path)]))
./root folder
for path in search(root, "file c3"):
  print("/".join([".", *map(lambda f: f.name, path)]))
./root folder/folder a3/folder b1/file c3
./root folder/folder a3/folder b4/file c3

Как всегда прекрасный ответ! Возможным расширением может быть передача полного пути элемента в предикат либо вместо имени (менее удобно, но проще), либо в качестве второго, несколько избыточного аргумента (наоборот).

Scott Sauyet 09.05.2022 18:16

спасибо Скотт. это определенно позволило бы выполнять более мощные поиски, например, указывать сегменты пути для соответствия критериям и подстановки. Хорошее упражнение для читателя :D

Mulan 09.05.2022 20:09

Большое спасибо @Mulan за отличную иллюстрацию. Вы не только ответили на мой вопрос, но и помогли мне улучшить мой текущий код. Ваше здоровье

Amrovic 09.05.2022 20:46

Другие вопросы по теме