При этом, нужно понимать, что аналогичная проблема, в целом, есть и у миксинов, и с какой-то точки зрения, она даже страшнее, ведь миксины не создают явного наследования, но при этом все ещё создают зависимость.
А в чем проблема то? Ну да, явного наследования нет, но зависим от миксина, что в этом плохого то? То что зависим не от абстракции? В наследовании мы тоже можем зависеть не от абстракции, при паттерне шаблон-стратегия. В общем, не вижу тут проблемы 😄
P.S. буду рад, если меня кто-то поправит, я много лет уже пытаюсь понять в чем преимущество миксинов :D
Я не берусь рассуждать о том, что написано в этой статье, но вот про преимущество миксинов сказать хочется)
На мой взгляд это действительно классная штука, и это что-то вроде попытки сделать множественное наследование безопасным. При должном опыте и/или усердии, можно грамотно нарезать функционал на миксины, и потом собирать из них как из кубиков различные классы, с разным поведением. Если добавить к ним еще интерфейсы, и учесть что миксинами можно затыкать реализации интерфейсов, то получается вообще классно. Как грубый пример, можно создать интерфейс монстра, монстр может атаковать, и монстр может двигаться. Но у вас есть по 10 различных идей, как именно монстр может двигаться, и по 10 как атаковать. Реализовав по 10 миксинов для обоих случаев, вы получаете 100 классов-монстров, с любой комбинацией этих двух действий, как только захочется. И чтобы создать очередной такой класс, достаточно всего то, написать что-то вроде
class SomeMonster with SomeAttackMixin, SomeMoveMixin implements IMonster {}
Да, конечно можно сказать, что тоже самое можно было бы сделать компонентным подходом, подпихивая через DI оба класса с реализованным поведением для каждого действия, и проксируя их в самом классе, при реализации интерфейса. Но с миксинами имхо это выглядит поизящнее и погибче, во первых потому что хорошо когда интерфейс состоит из двух методов, и ты затыкаешь его двумя миксинами, и соответственно двумя прокси методами через DI, совсем другое, когда таких методов 10, и рождается куча бойлерплейт кода, по типу
void attack() {
_attackService.attack();
}
Во вторых, представьте что у вас есть монстры, которые в середине атаки вынуждены, например, подзарядиться энергией, тогда некоторые методы атаки будут выглядеть как: 1. начало атаки 2. подзарядка 3. конец атаки
Все бы хорошо, но подзарядка внезапно, у всех монстров может проявляться совсем по разному, кто-то идет и жрет что-нибудь, кто-то просто стоит ждет, итд. В случае с миксинами, это не создает особо больших проблем, миксин, который бы решил подобную проблему, мог бы выглядеть примерно так:
соответственно, метод feed может реализовать либо миксин, который подмешивается вслед за миксином SomeAttackMixin, либо уже конечный класс. В первом случае, можно насоздавать еще 10 миксинов, реализующих feed метод (используя конструкцию on для миксинов, или implement, смотря что вам больше подходит). И эти 10 миксинов с feed методом, могут начать участвовать в комбинациях with, по созданию конечного класса. А вот в случае компонентного подхода, уже начинаются сложности, по типу инжектировать в конечный класс другой класс, в который инжектировать еще один...Выглядит более громоздко чем с миксинами, и правки в такие матрешки вносить, имхо, труднее.
Я понимаю, что пример достаточно надуманный, более реальный мне как-то затруднительно привести, надо думать. Но мысль я думаю ясна, и в реальной жизни я думаю такое вполне себе встречается
Ну, сами видите, более очевидным его назвать точно нельзя, ну а приоритетные очереди и деревья обычно уже реализованы в стандартных библиотеках языков, то что автор реализует это вручную, проблема языка, если там такого нету. + Вариант с приоритетной очередью кмк более популярен, и чаще встречается во всяких задачах итд, т.к. графы чаще разреженные, чем плотные, имхо
Это как раз и есть тот самый Прим за квадрат, но хочу заметить, что ваш изначальный комментарий был про то, что проще обойтись массивом (приоритетная очередь менее очевидное решение, как вы писали), но ваш вариант с поддержкой минимальных ребер не проще, а может быть даже сложнее. И еще раз повторюсь, он хорош только для плотных графов, для разреженных, очередь с приоритетом лучший выбор
По поводу извлечения, да, перепутал с просмотром, вы правы, за log размера, но это не влияет на асимптотику, в итоге все равно O(m*log m).
По поводу добавления просто опечатка, конечно за log размера, что вообще-то даже меньше чем за размер кучи.
Вы никак не учли перебор соседних вершин, квадрат возникает там
Вот здесь уже вы не правы, там нигде не возникает квадратов. Перебор соседних вершин на каждом шаге учтен амортизировано. То есть я говорю, что суммарно мы положим в очередь m раз, потому что всего ребер m, а на каждом шаге мы перебираем только соседей, а не все ребра, если у автора матрица смежности, а не список смежности, это другой вопрос, я не вникал в детали, и говорю про классического Прима.
Про извлечение вершин - да, хорошо, нужно извлечь все, ну и что? Это не влияет на асимптотику, она все равно O(m * log m), или O(m * log n), если как вы говорите, научиться обновлять элемент, а не добавлять новый, а это можно сделать, если например использовать бинарное дерево, которое по асимптотике дает все тоже самое, только бонусом умеет обновлять элемент (тоже за log)
P.S.: посмотрел, у автора действительно матрица смежности, но это проблема автора, а не алгоритма. Вы могли указать на это, а не предлагать вообще другой вариант, подходящий для плотных графов.
для каждого элемента из очереди вы будете перебирать все вершины графа
Почему, нет. Извлечение из очереди работает за O(1), а добавление за O(размер очереди), то есть O(log m)/O(log n) в зависимости от реализации. Суммарно мы добавим в очередь m раз (так как ребер m, и мы каждое ребро обработаем не больше одного раза). Извлечем n раз, так как остов размера n, извлечение O(1), поэтому итого O(n), итого весь алгоритм O(m * log m + n), где n по правилам асимптотики можно опустить, если считать, что m примерно равно n, а это вероятно так, для задачи остова.
откуда m? я думаю, что это n²
На каждом шаге (а таких шагов у вас будет n, так как вы будете добавлять в остов по одной вершине), вы должны перебрать ВСЕ ребра. Как вы собрались за O(1) определить для каждой вершины минимальное ребро (при этом, которое не входит в остов, что важно)? Магия?
P.S.: Алгоритм Прима за O(nˆ2) действительно существует, но по вашим комментариям, мне показалось, что вы говорите не о нем, и опять же, алгоритм за O(nˆ2), хорош для плотных графов. На разреженных графах он будет проигрывать варианту с приоритетной очередью
Уточню: в статье идет перебор не всех ребер, а только исходящих из добавляемой в остов вершины, то есть соседей. В вашей версии без priority_queue, вы не можете делать тоже самое, т. к. не факт, что следующая вершина на добавление в остов, будет лежать среди соседей добавляемой. На каждом шаге вам нужно либо перебирать ВСЕ вершины, либо иметь под рукой priority_queue, который любезно расскажет, у какой вершины минимальное ребро, из текущего остова
Я конечно может что то не так понял в статье, детально ее изучать не стал, просто бегло просмотрел. Но в классическом Приме в priority queue добавляются вершины, при очередном добавлении вершины в остовное дерево, по моему у автора тоже такая реализация. Примерно происходит следующее:
Добавляем в остов первую любую вершину, и всех ее соседей.
Из priority queue достаём минимальную
Добавляем ее в остов
Проходимся по соседям добавленной в остов вершины, и кладём их в очередь.
Если в остов входят не все вершины, то возвращаемся к шагу 2
При таком подходе, будет в худшем случае m добавлений в priority queue, где m — количество ребер. Что дает асимптотику m*log m или m*log n, в зависимости от реализации. Против вашей n * m. Даже при плохой реализации с priority queue, ваш вариант выгоден только при n < log m, то есть на очень плотных графах
Спасибо за статью! Очень не хватает примеров того, какое поведение будет нетипично для веба, как вы написали в самом начале. Чтобы иметь понимание, на что можно "напороться", если решить использовать flutter для веба. Ну и про производительность было бы круто почитать, на сколько велики требования к производительности клиента, и на сколько flutter проигрывает в производительности другим решениям: react/angular/vue
Будет очень круто, увидеть такие примеры в следующей статье (:
Да, я выше поправился, и уже написал, что это просто придирки, ниже в комментариях есть гораздо более ценные замечания. А вы отметили это от безысходности
Нашел место про которое вы говорите, вы придираетесь. Это пояснение в скобках, а до этого там написано "... за константное время". А про карму, очевидная отмазка перед лицом отсутствия информации. Я и без кармы разложил вам по полочкам, в какой части ваших рассуждений ошибка.
Ну тогда с нетерпением жду примеров. Ну и даже если так, то бинарный поиск уж точно не входит в их число. Я сдал приличное число задач на бин. поиск, что на литкоде, что в других местах, и на разных языках. Ни разу не столкнулся даже с намеком на то, что О-нотация посчитанная до сдачи задачи, разошлась бы с реальностью.
И еще, поправьте меня пожалуйста, возможно я плохо прочитал статью, но где в ней говорится, что время доступа к элементу одинаковое, как вы упомянули в самом начале? Я нашел лишь, что доступ к элементам происходит за константное время, вы с этим не согласны? Хотите сказать, что существуют какие-то супер модные процессоры, про которые видимо знает ОЧЕНЬ ограниченный круг людей, в которых время доступа к элементу массива зависит от его размера? Даже если время увеличивается "раз в сто", или даже в 200, это по прежнему остаётся константой (с точки зрения О-нотации). Так что зря вы жалуетесь на такие статьи, оказывается даже вам, есть что подчерпнуть из них, хоть и в комментариях
Во всех известных мне языках программирования, время доступа к элементу обычного массива — константа, с точки зрения О-нотации. Поэтому это не имеет значения, а вы просто пытаетесь умничать, извините
Может лучше укажете где грубые допущения/неверные утверждения? Да, статей про бинарный поиск большая куча, но может, именно эта статья, поможет кому-то лучше понять бин. поиск, а если кто-то его уже знает/ему не интересно, то просто не будет читать?
Можно было бы пойти дальше, и сказать, что в вашем коде, при наличии нескольких искомых чисел в массиве, будет находиться рандомный индекс из блока одинаковых чисел (в том смысле, что результат на разных массивах будет разный, где-то вы вернете первое число, где-то среднее, где-то последнее, итд). Можно было бы подумать/написать, как изменить код так, чтобы находился всегда первый/всегда последний, и при запуске обоих вариантов, вообще отрезок (:
Просто это на самом деле, было бы читать интереснее, чем про то, как устроена память компьютера. Те кого интересует бин. поиск, имхо и так это уже должны понимать
Более правильное определение бинарного поиска, особенно в вашей задаче, было бы таким - бинарный поиск, это поисковой алгоритм, который позволяет находить искомую точку на монотонной функции f(x). Зато не нужно рассказывать, что здесь есть какой-то неявный массив.
Задача предполагается для новичков, зачем усложнять код делением вместо умножения, если здесь спокойно можно использовать int64, и код становится сильно проще? Да, можно сказать, что мы экономим память, но во первых, много ли вы экономите? Во вторых, вы при этом жертвуете скоростью (деление дольше умножения), и читаемостью (умножение проще воспринимать, потому что там нет отбрасывания остатка, которое есть при делении).
Если правильно написать бин поиск, то костыль в виде переменной result, которую вы используете, станет не нужен, я не буду писать много букав, просто прикреплю код, в котором ее нет, и в котором используется более читаемый вариант с умножением. Это С++, но особо разницы я думаю нет, просто здесь long long - это int64, остальное, я думаю, все как в Java
class Solution {
public:
int mySqrt(int x) {
long long l = 0, r = x;
while (l < r) {
long long mid = (l + r + 1) / 2;
if (mid * mid > x) {
r = mid - 1;
} else {
l = mid;
}
}
return r;
}
};
P. S.: про пункт 2 - если бы авторы хотели сделать задачу сложнее, они бы вместо int во входных данных, поставили int64, и вот тогда, нужно было бы напрягаться с делением, вместо умножения
Мне кажется это у всех по разному. Я перешел с React на Flutter, просто когда выбирал на чем писать кроссплатформу, увидел что React Native просаживает по скорости, и сделал выбор в пользу flutter. И кстати достаточно быстро просек фишку (понятно что тонкости реактивного программирования можно еще изучать месяцами, а то и годами, но приложеньки уже с таким уровнем можно вполне писать).
Про гибкость, извините, не соглашусь, во Flutter можно делать все тоже самое, да, отличается принцип рендеринга дерева компонентов/виджетов, но это не значит, что веб гибче flutter. У меня даже сложилось впечатление, что flutter в чем-то гибче.
Про близость тоже спорно, мне кажется это тоже у кого как. Dart ведь почти копия java-script во многих моментах (его улучшенная версия, если послушать гугл). Посмотрите на Promise в JavaScript и на Future в Dart. Event Loop есть и там и там. Компонентный подход? Тоже и там и там (:
А в чем проблема то? Ну да, явного наследования нет, но зависим от миксина, что в этом плохого то? То что зависим не от абстракции? В наследовании мы тоже можем зависеть не от абстракции, при паттерне шаблон-стратегия. В общем, не вижу тут проблемы 😄
Я не берусь рассуждать о том, что написано в этой статье, но вот про преимущество миксинов сказать хочется)
На мой взгляд это действительно классная штука, и это что-то вроде попытки сделать множественное наследование безопасным. При должном опыте и/или усердии, можно грамотно нарезать функционал на миксины, и потом собирать из них как из кубиков различные классы, с разным поведением. Если добавить к ним еще интерфейсы, и учесть что миксинами можно затыкать реализации интерфейсов, то получается вообще классно. Как грубый пример, можно создать интерфейс монстра, монстр может атаковать, и монстр может двигаться. Но у вас есть по 10 различных идей, как именно монстр может двигаться, и по 10 как атаковать. Реализовав по 10 миксинов для обоих случаев, вы получаете 100 классов-монстров, с любой комбинацией этих двух действий, как только захочется. И чтобы создать очередной такой класс, достаточно всего то, написать что-то вроде
Да, конечно можно сказать, что тоже самое можно было бы сделать компонентным подходом, подпихивая через DI оба класса с реализованным поведением для каждого действия, и проксируя их в самом классе, при реализации интерфейса. Но с миксинами имхо это выглядит поизящнее и погибче, во первых потому что хорошо когда интерфейс состоит из двух методов, и ты затыкаешь его двумя миксинами, и соответственно двумя прокси методами через DI, совсем другое, когда таких методов 10, и рождается куча бойлерплейт кода, по типу
Во вторых, представьте что у вас есть монстры, которые в середине атаки вынуждены, например, подзарядиться энергией, тогда некоторые методы атаки будут выглядеть как:
1. начало атаки
2. подзарядка
3. конец атаки
Все бы хорошо, но подзарядка внезапно, у всех монстров может проявляться совсем по разному, кто-то идет и жрет что-нибудь, кто-то просто стоит ждет, итд.
В случае с миксинами, это не создает особо больших проблем, миксин, который бы решил подобную проблему, мог бы выглядеть примерно так:
соответственно, метод feed может реализовать либо миксин, который подмешивается вслед за миксином SomeAttackMixin, либо уже конечный класс. В первом случае, можно насоздавать еще 10 миксинов, реализующих feed метод (используя конструкцию on для миксинов, или implement, смотря что вам больше подходит). И эти 10 миксинов с feed методом, могут начать участвовать в комбинациях with, по созданию конечного класса.
А вот в случае компонентного подхода, уже начинаются сложности, по типу инжектировать в конечный класс другой класс, в который инжектировать еще один...Выглядит более громоздко чем с миксинами, и правки в такие матрешки вносить, имхо, труднее.
Я понимаю, что пример достаточно надуманный, более реальный мне как-то затруднительно привести, надо думать. Но мысль я думаю ясна, и в реальной жизни я думаю такое вполне себе встречается
Не поспоришь (:
Тут вы абсолютно правы)
Ну, сами видите, более очевидным его назвать точно нельзя, ну а приоритетные очереди и деревья обычно уже реализованы в стандартных библиотеках языков, то что автор реализует это вручную, проблема языка, если там такого нету. + Вариант с приоритетной очередью кмк более популярен, и чаще встречается во всяких задачах итд, т.к. графы чаще разреженные, чем плотные, имхо
Да, видимо действительно не так друг друга поняли 😁
Это как раз и есть тот самый Прим за квадрат, но хочу заметить, что ваш изначальный комментарий был про то, что проще обойтись массивом (приоритетная очередь менее очевидное решение, как вы писали), но ваш вариант с поддержкой минимальных ребер не проще, а может быть даже сложнее. И еще раз повторюсь, он хорош только для плотных графов, для разреженных, очередь с приоритетом лучший выбор
По поводу извлечения, да, перепутал с просмотром, вы правы, за log размера, но это не влияет на асимптотику, в итоге все равно O(m*log m).
По поводу добавления просто опечатка, конечно за log размера, что вообще-то даже меньше чем за размер кучи.
Вот здесь уже вы не правы, там нигде не возникает квадратов. Перебор соседних вершин на каждом шаге учтен амортизировано. То есть я говорю, что суммарно мы положим в очередь m раз, потому что всего ребер m, а на каждом шаге мы перебираем только соседей, а не все ребра, если у автора матрица смежности, а не список смежности, это другой вопрос, я не вникал в детали, и говорю про классического Прима.
Про извлечение вершин - да, хорошо, нужно извлечь все, ну и что? Это не влияет на асимптотику, она все равно O(m * log m), или O(m * log n), если как вы говорите, научиться обновлять элемент, а не добавлять новый, а это можно сделать, если например использовать бинарное дерево, которое по асимптотике дает все тоже самое, только бонусом умеет обновлять элемент (тоже за log)
P.S.: посмотрел, у автора действительно матрица смежности, но это проблема автора, а не алгоритма. Вы могли указать на это, а не предлагать вообще другой вариант, подходящий для плотных графов.
Почему, нет. Извлечение из очереди работает за O(1), а добавление за O(размер очереди), то есть O(log m)/O(log n) в зависимости от реализации. Суммарно мы добавим в очередь m раз (так как ребер m, и мы каждое ребро обработаем не больше одного раза). Извлечем n раз, так как остов размера n, извлечение O(1), поэтому итого O(n), итого весь алгоритм O(m * log m + n), где n по правилам асимптотики можно опустить, если считать, что m примерно равно n, а это вероятно так, для задачи остова.
На каждом шаге (а таких шагов у вас будет n, так как вы будете добавлять в остов по одной вершине), вы должны перебрать ВСЕ ребра. Как вы собрались за O(1) определить для каждой вершины минимальное ребро (при этом, которое не входит в остов, что важно)? Магия?
P.S.: Алгоритм Прима за O(nˆ2) действительно существует, но по вашим комментариям, мне показалось, что вы говорите не о нем, и опять же, алгоритм за O(nˆ2), хорош для плотных графов. На разреженных графах он будет проигрывать варианту с приоритетной очередью
Уточню: в статье идет перебор не всех ребер, а только исходящих из добавляемой в остов вершины, то есть соседей. В вашей версии без priority_queue, вы не можете делать тоже самое, т. к. не факт, что следующая вершина на добавление в остов, будет лежать среди соседей добавляемой. На каждом шаге вам нужно либо перебирать ВСЕ вершины, либо иметь под рукой priority_queue, который любезно расскажет, у какой вершины минимальное ребро, из текущего остова
Я конечно может что то не так понял в статье, детально ее изучать не стал, просто бегло просмотрел. Но в классическом Приме в priority queue добавляются вершины, при очередном добавлении вершины в остовное дерево, по моему у автора тоже такая реализация. Примерно происходит следующее:
Добавляем в остов первую любую вершину, и всех ее соседей.
Из priority queue достаём минимальную
Добавляем ее в остов
Проходимся по соседям добавленной в остов вершины, и кладём их в очередь.
Если в остов входят не все вершины, то возвращаемся к шагу 2
При таком подходе, будет в худшем случае m добавлений в priority queue, где m — количество ребер. Что дает асимптотику m*log m или m*log n, в зависимости от реализации. Против вашей n * m. Даже при плохой реализации с priority queue, ваш вариант выгоден только при n < log m, то есть на очень плотных графах
Проблема вашего предложения кроется в словосочетании "перебирать все". Да, так можно, но это долго по времени
Спасибо за статью! Очень не хватает примеров того, какое поведение будет нетипично для веба, как вы написали в самом начале. Чтобы иметь понимание, на что можно "напороться", если решить использовать flutter для веба. Ну и про производительность было бы круто почитать, на сколько велики требования к производительности клиента, и на сколько flutter проигрывает в производительности другим решениям: react/angular/vue
Будет очень круто, увидеть такие примеры в следующей статье (:
Да, я выше поправился, и уже написал, что это просто придирки, ниже в комментариях есть гораздо более ценные замечания. А вы отметили это от безысходности
Нашел место про которое вы говорите, вы придираетесь. Это пояснение в скобках, а до этого там написано "... за константное время". А про карму, очевидная отмазка перед лицом отсутствия информации. Я и без кармы разложил вам по полочкам, в какой части ваших рассуждений ошибка.
Ну тогда с нетерпением жду примеров. Ну и даже если так, то бинарный поиск уж точно не входит в их число. Я сдал приличное число задач на бин. поиск, что на литкоде, что в других местах, и на разных языках. Ни разу не столкнулся даже с намеком на то, что О-нотация посчитанная до сдачи задачи, разошлась бы с реальностью.
И еще, поправьте меня пожалуйста, возможно я плохо прочитал статью, но где в ней говорится, что время доступа к элементу одинаковое, как вы упомянули в самом начале? Я нашел лишь, что доступ к элементам происходит за константное время, вы с этим не согласны? Хотите сказать, что существуют какие-то супер модные процессоры, про которые видимо знает ОЧЕНЬ ограниченный круг людей, в которых время доступа к элементу массива зависит от его размера? Даже если время увеличивается "раз в сто", или даже в 200, это по прежнему остаётся константой (с точки зрения О-нотации). Так что зря вы жалуетесь на такие статьи, оказывается даже вам, есть что подчерпнуть из них, хоть и в комментариях
Во всех известных мне языках программирования, время доступа к элементу обычного массива — константа, с точки зрения О-нотации. Поэтому это не имеет значения, а вы просто пытаетесь умничать, извините
Может лучше укажете где грубые допущения/неверные утверждения? Да, статей про бинарный поиск большая куча, но может, именно эта статья, поможет кому-то лучше понять бин. поиск, а если кто-то его уже знает/ему не интересно, то просто не будет читать?
Хорошая статья, подробная (даже слишком 😁)
Можно было бы пойти дальше, и сказать, что в вашем коде, при наличии нескольких искомых чисел в массиве, будет находиться рандомный индекс из блока одинаковых чисел (в том смысле, что результат на разных массивах будет разный, где-то вы вернете первое число, где-то среднее, где-то последнее, итд). Можно было бы подумать/написать, как изменить код так, чтобы находился всегда первый/всегда последний, и при запуске обоих вариантов, вообще отрезок (:
Просто это на самом деле, было бы читать интереснее, чем про то, как устроена память компьютера. Те кого интересует бин. поиск, имхо и так это уже должны понимать
Более правильное определение бинарного поиска, особенно в вашей задаче, было бы таким - бинарный поиск, это поисковой алгоритм, который позволяет находить искомую точку на монотонной функции f(x). Зато не нужно рассказывать, что здесь есть какой-то неявный массив.
Задача предполагается для новичков, зачем усложнять код делением вместо умножения, если здесь спокойно можно использовать int64, и код становится сильно проще? Да, можно сказать, что мы экономим память, но во первых, много ли вы экономите? Во вторых, вы при этом жертвуете скоростью (деление дольше умножения), и читаемостью (умножение проще воспринимать, потому что там нет отбрасывания остатка, которое есть при делении).
Если правильно написать бин поиск, то костыль в виде переменной result, которую вы используете, станет не нужен, я не буду писать много букав, просто прикреплю код, в котором ее нет, и в котором используется более читаемый вариант с умножением. Это С++, но особо разницы я думаю нет, просто здесь long long - это int64, остальное, я думаю, все как в Java
P. S.: про пункт 2 - если бы авторы хотели сделать задачу сложнее, они бы вместо int во входных данных, поставили int64, и вот тогда, нужно было бы напрягаться с делением, вместо умножения
Мне кажется это у всех по разному. Я перешел с React на Flutter, просто когда выбирал на чем писать кроссплатформу, увидел что React Native просаживает по скорости, и сделал выбор в пользу flutter. И кстати достаточно быстро просек фишку (понятно что тонкости реактивного программирования можно еще изучать месяцами, а то и годами, но приложеньки уже с таким уровнем можно вполне писать).
Про гибкость, извините, не соглашусь, во Flutter можно делать все тоже самое, да, отличается принцип рендеринга дерева компонентов/виджетов, но это не значит, что веб гибче flutter. У меня даже сложилось впечатление, что flutter в чем-то гибче.
Про близость тоже спорно, мне кажется это тоже у кого как. Dart ведь почти копия java-script во многих моментах (его улучшенная версия, если послушать гугл). Посмотрите на Promise в JavaScript и на Future в Dart. Event Loop есть и там и там. Компонентный подход? Тоже и там и там (: