wenmo8 发布的文章

前言

很多同学将虚拟列表当做亮点写在简历上面,但是却不知道如何手写,那么这个就不是加分项而是减分项了。在上一篇文章欧阳教会你
如何实现一个定高虚拟列表
,但是实际项目中更多的是
不定高虚拟列表
,这篇文章欧阳来教你不定高如何实现。PS:建议先看看欧阳的上一篇
如何实现一个定高虚拟列表
后再来看这篇效果更佳。

欧阳也在找工作,坐标成都求内推!

什么是不定高虚拟列表

不定高的意思很简单,就是不知道每一项item的具体高度,如下图:
v1

现在我们有个问题,
在不定高的情况下我们就不能根据当前滚动条的
scrollTop
去计算可视区域里面实际渲染的第一个item的index位置,也就是
start
的值。

没有
start
,那么就无法实现在滚动的时候只渲染可视区域的那几个item了。

预估高度

既然我们不知道每个item的高度,那么就采用
预估高度
的方式去实现。比如这样:

const { listData, itemSize } = defineProps({
  // 列表数据
  listData: {
    type: Array,
    default: () => [],
  },
  // 预估item高度,不是真实item高度
  itemSize: {
    type: Number,
    default: 300,
  },
});

还是和上一篇一样的套路,计算出当前可视区域的高度
containerHeight
,然后结合预估的
itemSize
就可以得到当前可视区域里面渲染的item数量。代码如下:

const renderCount = computed(() => Math.ceil(containerHeight.value / itemSize));

注意:由于我们是预估的高度,所以这个
renderCount
的数量是不准的。

如果预估的高度比实际高太多,那么实际渲染的item数量就会不够,导致页面下方出现白屏的情况。

如果预估的高度太小,那么这里的item数量就会渲染的太多了,性能又没之前那么好。

所以预估item高度需要根据实际业务去给一个适当的值,理论上是宁可预估小点,也不预估的大了(大了会出现白屏)。

start初始值为0,并且算出了
renderCount
,此时我们也就知道了可视区域渲染的最后一个
end
的值。如下:

const end = computed(() => start.value + renderCount.value);

和上一篇一样计算end时在下方多渲染了一个item,第一个item有一部分滚出可视区域的情况时,如果不多渲染可能就会出现白屏的情况。

有了
start

end
,那么就知道了可视区域渲染的
renderList
,代码如下:

const renderList = computed(() => listData.slice(start.value, end.value + 1));

这样我们就知道了,初始化时可视区域应该渲染哪些item了,但是因为我们之前是给每个item
预估高度
,所以我们应该将这些高度的值
纠正过来

更新高度

为了记录不定高的list里面的每个item的高度,所以我们需要一个数组来存每个item的高度。所以我们需要定义一个
positions
数组来存这些值。

既然都存了每个item的高度,那么同样可以使用
top

bottom
这两个字段去记录每个item在列表中的
开始位置

结束位置
。注意
bottom - top
的值肯定等于
height
的值。

还有一个
index
字段记录每个item的index的值。
positions
定义如下:

const positions = ref<
  {
    index: number;
    height: number;
    top: number;
    bottom: number;
  }[]
>([]);

positions
的初始化值为空数组,那么什么时候给这个数组赋值呢?

答案很简单,虚拟列表渲染的是props传入进来的
listData
。所以我们watch监听
listData
,加上
immediate: true
。这样就可以实现初始化时给
positions
赋值,代码如下:

watch(() => listData, initPosition, {
  immediate: true,
});

function initPosition() {
  positions.value = [];
  listData.forEach((_item, index) => {
    positions.value.push({
      index,
      height: itemSize,
      top: index * itemSize,
      bottom: (index + 1) * itemSize,
    });
  });
}

遍历
listData
结合预估的
itemSize
,我们就可以得出每一个item里面的
height

top

bottom
这几个字段的值。

还有一个问题,我们需要一个元素来撑开滚动条。在定高的虚拟列表中我们是通过
itemSize * listData.length
得到的。显然这里不能那样做了,由于
positions
数组中存的是所有item的位置,
那么最后一个item的bottom的值就是列表的真实高度
。前面也是不准的,会随着我们纠正
positions
中的值后他就是越来越准的了。

所以列表的真实高度为:

const listHeight = computed(
  () => positions.value[positions.value.length - 1].bottom
);

此时
positions
数组中就已经记录了每个item的具体位置,虽然这个位置是错的。接下来我们就需要将这些错误的值纠正过来,如何纠正呢?

答案很简单,使用Vue的
onUpdated
钩子函数,这个钩子函数会在
响应式状态变更而更新其 DOM 树之后调用。
也就是会在
renderList
渲染成DOM后触发!

此时这些item已经渲染成了DOM节点,那么我们就可以遍历这些item的DOM节点拿到每个item的真实高度。都知道每个item的真实高度了,那么也就能够更新里面所有item的
top

bottom
了。代码如下:

<template>
  <div ref="container" class="container" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper" :style="{ transform: getTransform }">
      <div
        class="card-item"
        v-for="item in renderList"
        :key="item.index"
        ref="itemRefs"
        :data-index="item.index"
      >
        <span style="color: red"
          >{{ item.index }}
          <img width="200" :src="item.imgUrl" alt="" />
        </span>
        {{ item.value }}
      </div>
    </div>
  </div>
</template>

<script setup>
onUpdated(() => {
  updatePosition();
});

function updatePosition() {
  itemRefs.value.forEach((el) => {
    const index = +el.getAttribute("data-index");
    const realHeight = el.getBoundingClientRect().height;
    let diffVal = positions.value[index].height - realHeight;
    const curItem = positions.value[index];
    if (diffVal !== 0) {
      // 说明item的高度不等于预估值
      curItem.height = realHeight;
      curItem.bottom = curItem.bottom - diffVal;
      for (let i = index + 1; i < positions.value.length - 1; i++) {
        positions.value[i].top = positions.value[i].top - diffVal;
        positions.value[i].bottom = positions.value[i].bottom - diffVal;
      }
    }
  });
}
</script>

使用
:data-index="item.index"

index
绑定到item上面,更新时就可以通过
+el.getAttribute("data-index")
拿到对应item的
index

itemRefs
中存的是所有item的DOM元素,遍历他就可以拿到每一个item,然后拿到每个item在长列表中的
index
和真实高度
realHeight

diffVal的值是预估的高度比实际的高度大多少
,如果
diffVal
的值不等于0,说明预估的高度不准。此时就需要将当前item的高度
height
更新了,由于高度只会影响
bottom
的值,所以只需要更新当前item的
height

bottom

由于当前item的高度变了,假如
diffVal
的值为正值,说明我们预估的高度多了。此时我们需要从当前item的下一个元素开始遍历,直到遍历完整个长列表。我们预估多了,那么只需要将后面的所有item整体都向上移一移,移动的距离就是预估的差值
diffVal

所以这里需要从
index + 1
开始遍历,将遍历到的所有元素的
top

bottom
的值都减去
diffVal

将可视区域渲染的所有item都遍历一遍,将每个item的高度和位置都纠正过来,同时会将后面没有渲染到的item的
top

bottom
都纠正过来,这样就实现了高度的更新。理论上从头滚到尾,那么整个长列表里面的所有位置和高度都纠正完了。

开始滚动

通过前面我们已经实现了预估高度值的纠正,渲染过的item的高度和位置都是纠正过后的了。此时我们需要在滚动后如何计算出新的
start
的位置,以及
offset
偏移量的值。

还是和定高同样的套路,
当滚动条在item中间滚动时复用浏览器的滚动条,从一个item滚到另外一个item时才需要更新start的值以及offset偏移量的值。如果你看不懂这句话,建议先看我上一篇
如何实现一个定高虚拟列表
文章。

此时应该如何计算最新的
start
值呢?

很简单!在
positions
中存了两个字段分别是
top

bottom
,分别表示当前item的
开始位置

结束位置
。如果当前滚动条的
scrollTop
刚好在
top

bottom
之间,也就是
scrollTop >= top && scrollTop < bottom
,那么是不是就说明当前刚好滚到这个item的位置呢。

并且由于在
positions
数组中
bottom
的值是递增的,那么问题不就变成了查找第一个item的
scrollTop < bottom
。所以我们得出:

function getStart(scrollTop) {
  return positions.value.findIndex((item) => scrollTop < item.bottom);
}

每次scroll滚动都会触发一次这个查找,那么我们可以优化上面的算法吗?

positions
数组中的
bottom
字段是递增的,这很符合
二分查找
的规律。不了解二分查找的同学可以看看leetcode上面的这道题:
https://leetcode.cn/problems/search-insert-position/description/

所以上面的代码可以优化成这样:

function getStart(scrollTop) {
  let left = 0;
  let right = positions.value.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (positions.value[mid].bottom === scrollTop) {
      return mid + 1;
    } else if (positions.value[mid].bottom < scrollTop) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return left;
}

和定高的虚拟列表一样,当在
start
的item中滚动时直接复用浏览器的滚动,无需做任何事情。所以此时的
offset
偏移量就应该等于当前
start
的item的
top
值,也就是
start
的item前面的所有item加起来的高度。所以得出
offset
的值为:

offset.value = positions.value[start.value].top;

可能有的小伙伴会迷惑,在
start
的item中的滚动值为什么不算到
offset
偏移中去呢?

因为在
start
的item范围内滚动时都是直接使用的浏览器滚动,已经有了scrollTop,所以无需加到
offset
偏移中去。

所以我们得出当scroll事件触发时代码如下:

function handleScroll(e) {
  const scrollTop = e.target.scrollTop;
  start.value = getStart(scrollTop);
  offset.value = positions.value[start.value].top;
}

同样
offset
偏移值使用
translate3d
应用到可视区域的div上面,代码如下:

<template>
  <div ref="container" class="container" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper" :style="{ transform: getTransform }">
      ...省略
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  offset: {
    type: Number,
    default: 0,
  },
});
const getTransform = computed(() => `translate3d(0,${props.offset}px,0)`);
</script>

这个是最终的运行效果图:
demo

完整的父组件代码如下:

<template>
  <div style="height: 100vh; width: 100vw">
    <VirtualList :listData="data" :itemSize="50" />
  </div>
</template>

<script setup>
import VirtualList from "./dynamic.vue";
import { faker } from "@faker-js/faker";
import { ref } from "vue";

const data = ref([]);
for (let i = 0; i < 1000; i++) {
  data.value.push({
    index: i,
    value: faker.lorem.sentences(),
  });
}
</script>

<style>
html {
  height: 100%;
}
body {
  height: 100%;
  margin: 0;
}
#app {
  height: 100%;
}
</style>

完整的虚拟列表子组件代码如下:

<template>
  <div ref="container" class="container" @scroll="handleScroll($event)">
    <div class="placeholder" :style="{ height: listHeight + 'px' }"></div>
    <div class="list-wrapper" :style="{ transform: getTransform }">
      <div
        class="card-item"
        v-for="item in renderList"
        :key="item.index"
        ref="itemRefs"
        :data-index="item.index"
      >
        <span style="color: red"
          >{{ item.index }}
          <img width="200" :src="item.imgUrl" alt="" />
        </span>
        {{ item.value }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, onUpdated } from "vue";
const { listData, itemSize } = defineProps({
  // 列表数据
  listData: {
    type: Array,
    default: () => [],
  },
  // 预估item高度,不是真实item高度
  itemSize: {
    type: Number,
    default: 300,
  },
});

const container = ref(null);
const containerHeight = ref(0);
const start = ref(0);
const offset = ref(0);
const itemRefs = ref();
const positions = ref<
  {
    index: number;
    height: number;
    top: number;
    bottom: number;
  }[]
>([]);

const end = computed(() => start.value + renderCount.value);
const renderList = computed(() => listData.slice(start.value, end.value + 1));
const renderCount = computed(() => Math.ceil(containerHeight.value / itemSize));
const listHeight = computed(
  () => positions.value[positions.value.length - 1].bottom
);
const getTransform = computed(() => `translate3d(0,${offset.value}px,0)`);

watch(() => listData, initPosition, {
  immediate: true,
});

function handleScroll(e) {
  const scrollTop = e.target.scrollTop;
  start.value = getStart(scrollTop);
  offset.value = positions.value[start.value].top;
}

function getStart(scrollTop) {
  let left = 0;
  let right = positions.value.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (positions.value[mid].bottom === scrollTop) {
      return mid + 1;
    } else if (positions.value[mid].bottom < scrollTop) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return left;
}

function initPosition() {
  positions.value = [];
  listData.forEach((_item, index) => {
    positions.value.push({
      index,
      height: itemSize,
      top: index * itemSize,
      bottom: (index + 1) * itemSize,
    });
  });
}

function updatePosition() {
  itemRefs.value.forEach((el) => {
    const index = +el.getAttribute("data-index");
    const realHeight = el.getBoundingClientRect().height;
    let diffVal = positions.value[index].height - realHeight;
    const curItem = positions.value[index];
    if (diffVal !== 0) {
      // 说明item的高度不等于预估值
      curItem.height = realHeight;
      curItem.bottom = curItem.bottom - diffVal;
      for (let i = index + 1; i < positions.value.length - 1; i++) {
        positions.value[i].top = positions.value[i].top - diffVal;
        positions.value[i].bottom = positions.value[i].bottom - diffVal;
      }
    }
  });
}

onMounted(() => {
  containerHeight.value = container.value.clientHeight;
});

onUpdated(() => {
  updatePosition();
});
</script>

<style scoped>
.container {
  height: 100%;
  overflow: auto;
  position: relative;
}

.placeholder {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.card-item {
  padding: 10px;
  color: #777;
  box-sizing: border-box;
  border-bottom: 1px solid #e1e1e1;
}
</style>

总结

这篇文章我们讲了不定高的虚拟列表如何实现,首先给每个item设置一个预估高度
itemSize
。然后根据传入的长列表数据
listData
初始化一个
positions
数组,数组中的
top

bottom

height
等属性表示每个item的位置。然后根据可视区域的高度加上
itemSize
算出可视区域内可以渲染多少
renderCount
个item。接着就是在
onUpdated
钩子函数中根据每个item的实际高度去修正
positions
数组中的值。

在滚动时查找第一个item的bottom大于scrollTop,这个item就是
start
的值。
offset
偏移的值为
start

top
属性。

值得一提的是如果不定高的列表中有图片就不能在
onUpdated
钩子函数中修正
positions
数组中的值,而是应该监听图片加载完成后再去修正
positions
数组。可以使用
ResizeObserver
去监听渲染的这一堆item,注意
ResizeObserver
的回调会触发两次,第一次为渲染item的时候,第二次为item中的图片加载完成后。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

Solon 框架!

新一代,面向全场景的 Java 应用开发框架。
从零开始构建(非 java-ee 架构),有灵活的接口规范与开放生态

  • 追求: 更快、更小、更简单
  • 提倡: 克制、高效、开放、生态

有什么特点(相对传统方案)?

特点 描述
更高的计算性价比 并发高 300%;内存省 50%
更快的开发效率 代码少;入门快;启动快 10 倍(调试快)
更好的生产与部署体验 打包小 90%
更大的兼容范围 非 java-ee 架构;同时支持 java8 ~ java23,graalvm native image

最近更新了什么?

  • 新增 solon-data-rx-sqlutils 插件(基于 r2dbc 构建)
    • 可配合 solon-web-rx 或者 solon-cloud-gateway 使用
  • 添加 solon ClassUtil.scanClasses 方法
  • 添加 solon 非单例类使用生命周期时 warn 日志提醒
  • 添加 solon-cloud-gateway ExContext:toContext 方法,可用于支持经典接口接入(比如,sa-token 签权)
  • 添加 solon ContextHolder 替代 ContextUtil ,后者标为弃用
  • 添加 solon Context::isHeadersSent 方法,用于响应式转经典式后识别数据发送情况
  • 添加 solon SolonApp::isMain 方法,用于在单元测试时识别是否可同步到 System 属性集
  • 优化 solon ClassUtils.newInstance 异常类名显示
  • 优化 solon InjectGather:isMethod 条件(仅方法,之前构造也算),让
    @Bean
    方法的检测先于组件的构造器
  • 优化 solon-mvc Action 返回为 void,不作渲染处理
  • 优化 solon-data DsUtils 构建时支持 "@type" 属性申明(统一未来的配置类型申明风格)
  • 优化 solon-data DataSources 的配置获取时机
  • 优化 solon-data-sqlutils SqlUtilsFactory 接口设计
  • 优化 solon-scheduling Async 异常提示
  • 优化 solon-scheduling Retry 拦截优先级到最里层
  • 优化 solon-scheduling-simple 调用异常提示
  • 修复 solon-mvc 以实体接收时
    UploadedFile[]
    字段不能注入的问题
  • 修复 solon-boot-smarthttp 会把默认时区设为 GMT 的问题
  • snakc3 升为 3.2.122
  • redisx 升为 1.6.9
  • socketd 升为 2.5.14
  • folkmq 升为 1.7.10
  • redisson 升为 3.39.0
  • smarthttp 升为 2.5
  • mybatis-flex 升为 1.10.3
  • sqltoy 升为 5.6.34.jre8
  • slf4j 升为 2.0.16
  • log4j 升为 2.24.3
  • jansi 升为 2.4.1
  • fury 升为 0.9.0

项目架构图

项目仓库地址?

官网?

背景介绍

人工生命(AL:Artificial life)这一概念由美国计算机科学家、人工生命领域创始人之一克里斯托弗・盖尔・兰顿(Christopher G. Langton)提出。1986 年,兰顿提出了 “兰顿蚂蚁”(Langton's ant),它作为一个细胞自动机例子,是具有简单逻辑规则却能展现复杂动态行为的二维图灵机。次年,即 1987 年,在洛斯阿拉莫斯国家实验室(Los Alamos National Laboratory,制造了第一枚原子弹实验室)召开的 “生成以及模拟生命系统的国际会议” 上,兰顿正式提出了 “人工生命” 的概念,即使用计算机技术对生命建模,模拟生命系统。

目前人工生命的概念涵盖两个主要方面:其一为计算机科学范畴内的虚拟生命系统,这需要运用计算机软件工程以及人工智能技术来构建与实现;其二是借助基因工程技术对生物进行人工改造所形成的工程生物系统,其发展与合成生物学技术紧密相连,通过对生物遗传物质的精准操作与设计,赋予生物新的特性和功能,从而拓展生命的边界与可能性,推动生命科学在工程应用领域的进一步发展。

一、兰顿蚂蚁基本概念

  • 定义:兰顿蚂蚁是一个二维图灵机,由黑白格子和一只“蚂蚁”构成。
  • 提出者:克里斯托夫·兰顿
  • 特性:拥有非常简单的逻辑和复杂的表现,其图灵完备性在2000年被证明。

二、规则与行为模式

  • 规则:


    1. 在平面上的正方形格子中,每个格子被填上黑色或白色。
    2. 有一只“蚂蚁”位于其中一个格子上,其头部朝向上下左右其中一方。
    3. 若蚂蚁在白格上,则左转90度,将该格改为黑格,然后向前移一步。
    4. 若蚂蚁在黑格上,则右转90度,将该格改为白格,然后向前移一步。
  • 行为模式:


    1. 初始阶段:从全白的背景开始,蚂蚁在最初的数百步内会留下许多对称或重复的形状的路线。例如,假设蚂蚁初始位于一个全白平面的中心格点,头部朝上,第一步它左转90度(因为在白格上),将所在格染黑,然后向前一步。这样几步下来就可能形成一个简单的对称图案。
    2. 混沌阶段:随着步数的增加,蚂蚁的路线会变得类似混沌的假随机状态。这一阶段蚂蚁的行走路线看起来毫无规律,就像随意乱走一样。
    3. 高速公路阶段:大约经过一万步后,蚂蚁的路线会进入一个以104步为周期的无限重复的“高速公路”模式,并朝固定方向移动。如下图所示:

三、推广与扩展

  • 多种颜色:除了基本的黑白两色外,兰顿蚂蚁的概念也可以被扩展到使用多种颜色。每种颜色可以定义蚂蚁左转或右转的规则,通用的表示方法是用L和R依序表示各颜色是左转还是右转。
  • 其他形状:除了正方形格子外,也可以使用其他形状的格子,如六角形格子。
  • Turmites:进一步扩展是考虑Turing机器的多种状态,即蚂蚁本身的颜色可以改变。这些蚂蚁被称为“Turmites”,它们的行为模式包括产生高速公路、混乱增长和螺旋增长等。

四、意义与应用

  • 理论意义:兰顿蚂蚁展示了简单规则下产生的复杂行为,对于理解细胞自动机、复杂系统和图灵机等领域具有重要意义。
  • 实际应用:
    1. 计算布尔电路:兰顿蚂蚁的轨迹可以用于计算布尔电路,通过特定的初始配置,可以实现逻辑门的功能。比如在特定的黑白格初始布局下,蚂蚁的行走轨迹能对应布尔电路中的与、或、非等逻辑操作。
    2. 模拟图灵机:兰顿蚂蚁可以模拟任意图灵机进行计算,显示了其通用计算能力。例如,通过设计特定的初始状态,如设定某些格子的颜色以及蚂蚁的初始位置和朝向等,可以实现简单的图灵机算法。
    3. 模式生成:在艺术和设计领域,兰顿蚂蚁的轨迹可以生成独特的图案和纹理,用于装饰和创意设计。例如,将蚂蚁的行走轨迹记录下来,经过艺术化处理后可以成为独特的壁纸图案。

五、与其他模型的对比

  • 与Conway的生命游戏对比:Conway的生命游戏也是一个经典的细胞自动机模型,但它主要关注的是细胞的生存和繁殖规则,而兰顿蚂蚁则更注重个体行为的动态变化和路径生成。
  • 与元胞自动机对比:元胞自动机通常涉及多个细胞的状态变化,而兰顿蚂蚁则专注于单个“蚂蚁”的行为,尽管规则简单,但产生的行为却异常复杂。
  • 与图灵机对比:图灵机是一种通用计算模型,而兰顿蚂蚁通过简单的规则实现了图灵完备性,展示了简单系统中的复杂计算能力。

下面是代码:

兰顿蚂蚁html\go\php\python\java

每个版本都实现了相同的功能:

在网格上模拟兰顿蚂蚁的移动

使用黑白两色表示网格状态

用红色标记蚂蚁当前位置

支持周期性边界条件

HTML版本:

<!DOCTYPE html>
<html>
<head>
    <metahttp-equiv="Content-Type"content="text/html; charset=UTF-8">
    <title>兰顿蚂蚁</title>
    <style>html, body{margin:0 !important;padding:0 !important;
        }body{display:flex;justify-content:center;align-items:center;background-color:#555555;height:100vh;
        }canvas{display:block;background-color:white;
        }#temp{display:none;
        }
    </style>
</head>
<body>
    <canvasid="bg"width="640"height="640"></canvas>
    <canvasid="temp"width="640"height="640"></canvas>
    <script>
        //处理索引边界
const cycle=(idx, max)=>(idx+max)%max;//获取画布和上下文
const bg=document.getElementById("bg");
const temp
=document.getElementById("temp");
const ctx
=bg.getContext("2d");
const ctempx
=temp.getContext("2d");//设置基本参数 const bgWidth= 640;
const bgHeight
= 640;
const size
= 2;
const speed
= 100;
const width
=bgWidth/size;
const height
=bgHeight/size;//初始化画布尺寸 [bg, temp].forEach(canvas=>{
canvas.width
=bgWidth;
canvas.height
=bgHeight;
});
//蚂蚁初始位置(中心点) let antx=width/ 2;
let anty
=height/ 2;//方向数组:上、右、下、左 const dirs=[[0,-1], [1,0], [0,1], [-1,0]];
let dir
= 0;//初始化网格(使用数组技巧) let grid= newArray(height).fill(0).map(_=> newArray(width).fill(1));//移动函数(dir可以是负数或大于3) const move=(dir)=>{
dir
=cycle(dir, dirs.length);
antx
+=dirs[dir][0];
anty
+=dirs[dir][1];
antx
=cycle(antx, width);
anty
=cycle(anty, height);returndir;
}
//初始化画布设置 ctx.fillStyle= "white";
ctx.fillRect(
0,0, bgWidth, bgHeight);
ctx.scale(size, size);
ctx.imageSmoothingEnabled
= false;//单步执行函数 const step=()=>{
const px
=grid[anty][antx];//翻转当前格子的颜色,然后移动到下一个格子 if(px> 0) {
grid[anty][antx]
= 0;//变黑 dir=move(dir+ 1);//右转 }else{
grid[anty][antx]
= 7;//变白 dir=move(dir- 1);//左转 }
}
//颜色转换函数 const intToColor=(px)=>{//颜色对应关系: //0 -> 黑色 [0, 0, 0] * 255 //7 -> 白色 [1, 1, 1] * 255 //3 -> 红色 [1, 0, 0] * 255 //最后一个字节是透明度 return[px% 2 * 255, px% 3 * 255, px% 3 * 255,255];
}
//绘制函数 const paint=()=>{//在渲染前计算多步 for(let i= 0; i<speed; i++) {
step();
}
//用红色标记蚂蚁的位置 const tmp=grid[anty][antx];
grid[anty][antx]
= 3;
const bytes
=grid.flat().map(intToColor).flat();
const imgData
= newImageData(Uint8ClampedArray.from(bytes), width, height);
grid[anty][antx]
=tmp;
ctempx.putImageData(imgData,
0,0);
ctx.drawImage(temp,
0,0);
}
//设置动画循环 setInterval(paint,20);</script> </body> </html>

Python版本使用Pygame库实现图形界面,要运行这些代码你需要安装相应的依赖: Python: pip install pygame

importpygameimportnumpy as np#初始化 Pygame
pygame.init()#设置基本参数
BG_WIDTH = 640BG_HEIGHT= 640CELL_SIZE= 2SPEED= 100WIDTH= BG_WIDTH //CELL_SIZE
HEIGHT
= BG_HEIGHT //CELL_SIZE#创建窗口 screen =pygame.display.set_mode((BG_WIDTH, BG_HEIGHT))
pygame.display.set_caption(
"兰顿蚂蚁")#初始化网格 grid = np.ones((HEIGHT, WIDTH), dtype=int)#蚂蚁初始位置(中心点) ant_x = WIDTH // 2ant_y= HEIGHT // 2 #方向数组:上、右、下、左 DIRS = [(0, -1), (1, 0), (0, 1), (-1, 0)]
dir_idx
=0defmove(direction):globalant_x, ant_y
direction
= direction %len(DIRS)
ant_x
= (ant_x + DIRS[direction][0]) %WIDTH
ant_y
= (ant_y + DIRS[direction][1]) %HEIGHTreturndirectiondefstep():globaldir_idx
px
=grid[ant_y][ant_x]if px >0:
grid[ant_y][ant_x]
= 0 #变黑 dir_idx = move(dir_idx + 1) #右转 else:
grid[ant_y][ant_x]
= 7 #变白 dir_idx = move(dir_idx - 1) #左转 defint_to_color(px):if px == 0: #黑色 return(0, 0, 0)elif px == 7: #白色 return (255, 255, 255)else: #红色(蚂蚁位置) return (255, 0, 0)#主循环 running =True
clock
=pygame.time.Clock()whilerunning:for event inpygame.event.get():if event.type ==pygame.QUIT:
running
=False#计算多步 for _ inrange(SPEED):
step()
#绘制 for y inrange(HEIGHT):for x inrange(WIDTH):
color
=int_to_color(grid[y][x])
pygame.draw.rect(screen, color,
(x
* CELL_SIZE, y *CELL_SIZE,
CELL_SIZE, CELL_SIZE))
#标记蚂蚁位置 pygame.draw.rect(screen, (255, 0, 0),
(ant_x
* CELL_SIZE, ant_y *CELL_SIZE,
CELL_SIZE, CELL_SIZE))

pygame.display.flip()
clock.tick(
50)

pygame.quit()

Java版本使用Swing实现图形界面

import javax.swing.*;import java.awt.*;importjava.awt.image.BufferedImage;public class LangtonAnt extendsJPanel {private static final int BG_WIDTH = 640;private static final int BG_HEIGHT = 640;private static final int CELL_SIZE = 2;private static final int SPEED = 100;private static final int WIDTH = BG_WIDTH /CELL_SIZE;private static final int HEIGHT = BG_HEIGHT /CELL_SIZE;private int[][] grid;private intantX, antY, dir;private final int[][] dirs = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};privateBufferedImage buffer;publicLangtonAnt() {
setPreferredSize(
newDimension(BG_WIDTH, BG_HEIGHT));
initializeGrid();
buffer
= newBufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);

Timer timer
= new Timer(20, e ->{
update();
repaint();
});
timer.start();
}
private voidinitializeGrid() {
grid
= new int[HEIGHT][WIDTH];for (int y = 0; y < HEIGHT; y++) {for (int x = 0; x < WIDTH; x++) {
grid[y][x]
= 1;
}
}
antX
= WIDTH / 2;
antY
= HEIGHT / 2;
dir
= 0;
}
private int move(intdirection) {
direction
= (direction + dirs.length) %dirs.length;
antX
= (antX + dirs[direction][0] + WIDTH) %WIDTH;
antY
= (antY + dirs[direction][1] + HEIGHT) %HEIGHT;returndirection;
}
private voidstep() {int px =grid[antY][antX];if (px > 0) {
grid[antY][antX]
= 0; //变黑 dir = move(dir + 1); //右转 } else{
grid[antY][antX]
= 7; //变白 dir = move(dir - 1); //左转 }
}
private voidupdate() {for (int i = 0; i < SPEED; i++) {
step();
}
}

@Override
protected voidpaintComponent(Graphics g) {super.paintComponent(g);//更新缓冲图像 for (int y = 0; y < HEIGHT; y++) {for (int x = 0; x < WIDTH; x++) {
buffer.setRGB(x, y, grid[y][x]
> 0 ? 0xFFFFFF : 0);
}
}
buffer.setRGB(antX, antY,
0xFF0000); //蚂蚁位置标记为红色//绘制放大后的图像 g.drawImage(buffer, 0, 0, BG_WIDTH, BG_HEIGHT, null);
}
public static voidmain(String[] args) {
SwingUtilities.invokeLater(()
->{
JFrame frame
= new JFrame("兰顿蚂蚁");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(
newLangtonAnt());
frame.pack();
frame.setLocationRelativeTo(
null);
frame.setVisible(
true);
});
}
}

Go版本使用Ebiten游戏引擎实现图形界面,要运行这些代码你需要安装相应的依赖: Go: go get github.com/hajimehoshi/ebiten/v2

packagemainimport("image/color"
    "log"

    "github.com/hajimehoshi/ebiten/v2")const(
bgWidth
= 640bgHeight= 640cellSize= 2speed= 100width= bgWidth /cellSize
height
= bgHeight /cellSize
)
type Game struct{
grid [][]
intantXintantYintdirintdirs [][2]intpixels []byte}func NewGame() *Game {
g :
= &Game{
grid:
make([][]int, height),
antX: width
/ 2,
antY: height
/ 2,
dirs: [][
2]int{{0, -1}, {1, 0}, {0, 1}, {-1, 0}},
pixels:
make([]byte, width*height*4),
}
for y := rangeg.grid {
g.grid[y]
= make([]int, width)for x := rangeg.grid[y] {
g.grid[y][x]
= 1}
}
returng
}
func (g *Game) move(dir int) int{
dir
= (dir + len(g.dirs)) % len(g.dirs)
g.antX
= (g.antX + g.dirs[dir][0] + width) %width
g.antY
= (g.antY + g.dirs[dir][1] + height) %heightreturndir
}
func (g *Game) step() {
px :
=g.grid[g.antY][g.antX]if px > 0{
g.grid[g.antY][g.antX]
= 0 //变黑 g.dir = g.move(g.dir + 1) //右转 } else{
g.grid[g.antY][g.antX]
= 7 //变白 g.dir = g.move(g.dir - 1) //左转 }
}
func (g *Game) Update() error{for i := 0; i < speed; i++{
g.step()
}
returnnil
}
func (g *Game) Draw(screen *ebiten.Image) {//更新像素数据 for y := 0; y < height; y++{for x := 0; x < width; x++{
idx :
= (y*width + x) * 4 if g.grid[y][x] > 0{
g.pixels[idx]
= 255 //R g.pixels[idx+1] = 255 //G g.pixels[idx+2] = 255 //B g.pixels[idx+3] = 255 //A } else{
g.pixels[idx]
= 0 //R g.pixels[idx+1] = 0 //G g.pixels[idx+2] = 0 //B g.pixels[idx+3] = 255 //A }
}
}
//标记蚂蚁位置 idx := (g.antY*width + g.antX) * 4g.pixels[idx]= 255 //R g.pixels[idx+1] = 0 //G g.pixels[idx+2] = 0 //B g.pixels[idx+3] = 255 //A screen.WritePixels(g.pixels)
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {returnwidth, height
}
funcmain() {
ebiten.SetWindowSize(bgWidth, bgHeight)
ebiten.SetWindowTitle(
"兰顿蚂蚁")if err := ebiten.RunGame(NewGame()); err !=nil {
log.Fatal(err)
}
}

PHP版本使用GD库生成静态图像,要运行这些代码你需要安装相应的依赖: PHP: 需要安装GD扩展

<?phpclassLangtonAnt {private $width = 640;private $height = 640;private $cellSize = 2;private $speed = 100;private $gridWidth;private $gridHeight;private $grid =[];private $antX;private $antY;private $dir = 0;private $dirs = [[0, -1], [1, 0], [0, 1], [-1, 0]];private $image;public function__construct() {$this->gridWidth = $this->width / $this->cellSize;$this->gridHeight = $this->height / $this->cellSize;$this->antX = $this->gridWidth / 2;$this->antY = $this->gridHeight / 2;//初始化网格
        for ($y = 0; $y < $this->gridHeight; $y++) {$this->grid[$y] = array_fill(0, $this->gridWidth, 1);
}
//创建图像 $this->image = imagecreatetruecolor($this->width, $this->height);
imagefilledrectangle(
$this->image, 0, 0, $this->width, $this->height,imagecolorallocate($this->image, 255, 255, 255));
}
private function move($dir) {$dir = ($dir + count($this->dirs)) % count($this->dirs);$this->antX = ($this->antX + $this->dirs[$dir][0] + $this->gridWidth) % $this->gridWidth;$this->antY = ($this->antY + $this->dirs[$dir][1] + $this->gridHeight) % $this->gridHeight;return $dir;
}
private functionstep() {$px = $this->grid[$this->antY][$this->antX];if ($px > 0) {$this->grid[$this->antY][$this->antX] = 0; //变黑 $this->dir = $this->move($this->dir + 1); //右转 } else{$this->grid[$this->antY][$this->antX] = 7; //变白 $this->dir = $this->move($this->dir - 1); //左转 }
}
public function run($steps) {for ($i = 0; $i < $steps; $i++) {$this->step();
}
//绘制网格 for ($y = 0; $y < $this->gridHeight; $y++) {for ($x = 0; $x < $this->gridWidth; $x++) {$color = $this->grid[$y][$x] > 0 ?imagecolorallocate($this->image, 255, 255, 255) :imagecolorallocate($this->image, 0, 0, 0);

imagefilledrectangle(
$this->image, $x * $this->cellSize, $y * $this->cellSize,($x + 1) * $this->cellSize - 1,($y + 1) * $this->cellSize - 1, $color);
}
}
//标记蚂蚁位置 $red = imagecolorallocate($this->image, 255, 0, 0);
imagefilledrectangle(
$this->image, $this->antX * $this->cellSize, $this->antY * $this->cellSize,($this->antX + 1) * $this->cellSize - 1,($this->antY + 1) * $this->cellSize - 1, $red);//输出图像 header('Content-Type: image/png');
imagepng(
$this->image);
imagedestroy(
$this->image);
}
}
$ant = newLangtonAnt();$ant->run(10000); //运行10000步

综上所述,兰顿蚂蚁是一个具有深刻理论意义和广泛应用前景的细胞自动机模型。它通过简单的规则展示了复杂的行为模式,为我们理解自然界和人工系统中的复杂现象提供了新的视角和工具。未来的研究可以进一步探索兰顿蚂蚁在更多领域的应用,如生物系统的建模、城市交通规划等,以及开发新的扩展模型,以增强其计算能力和表现形式。

基础概念

这是
自动评估基准
系列文章的第一篇,敬请关注系列文章:

  • 基础概念
  • 设计你的自动评估任务
  • 一些评估测试集
  • 技巧与提示

注:本文内容与我写的 通用评估博客 存在部分重叠

什么是自动评估基准?

自动化基准测试通常按照以下方式工作:你希望了解你的模型在某些方面的表现。这些“某些方面”可以是一个明确定义的具体任务,例如“我的模型在垃圾邮件分类中的表现如何?”,也可以是一个更抽象和通用的能力,例如“我的模型的数学能力有多强?”。

基于此,你可以通过以下方式构建评估:

数据集:
数据集由多个样本组成。这些样本包含模型的输入,有时还包括一个参考答案(称为“gold”),用于与模型的输出进行比较。
样本的设计通常是为了尽量模拟你想测试模型的场景。例如,如果你在研究电子邮件分类,你可以创建一个包含垃圾邮件和非垃圾邮件的样本数据集,并尝试加入一些具有挑战性的边界案例等。

评估指标:
评估指标用于对模型进行评分。例如:你的模型对垃圾邮件的分类准确度如何?正确分类的样本得分为1,错误分类的得分为0。
评估指标使用模型的输出来进行评分。在大型语言模型(LLMs)的情况下,人们主要关注两种输出:

模型根据输入生成的文本(生成式评估,generative evaluation)
提供给模型的一个或多个序列的对数概率(多项选择评估,有时称为 MCQA,或者困惑度评估 perplexity evaluations)
有关更多信息,请查看
模型推理与评估页面

在模型没有见过 (即未出现在训练集) 的数据上进行评估会更有意义,得出的模型
泛化性
结论才更准确。比如在只见过假冒银行垃圾邮件的模型上测试其能否正确分类与 “健康” 相关的垃圾邮件。

注:
模型只能在训练数据上预测效果良好 (没有隐式地学习到更高层次的通用范式) 的现象叫做 过拟合 。这就类似于一个学生死记硬背了考试题目,却没有理解背后的知识点。所以只用训练集中的数据测试评估 LLM 得到的分数指标实际上是模型不具备的能力。

自动评估基准的优劣势

优势:

  • 一致性和可重复性
    :在同一个模型上运行相同的自动评估基准 10 次,测试结果也是相同的 (除非受到硬件或模型自身随机性的影响)。所以相同任务下,多个模型的测试排名结果是公正的。
  • 低成本规模效益
    :目前自动评估基准是评估模型成本最低的方式之一。
  • 易于理解
    :大部分自动化方式的评价指标理解起来都非常容易。
    例如:精确匹配可以理解为生成文本跟参考文本是否完全一致;准确率可以理解为做出的选项有多大程度是正确的 (不过对于像 BLEU ROUGE 这种评价方式,理解难度会稍微高一些)。
  • 高质量测试集
    :许多自动评估基准的测试集都来自专家级生成数据集或现有的高质量数据集 (如 MMLU 或 MATH)。当然也不是说这些测试集就完美无瑕,例如 MMLU 就被发现存在一些解析错误以及事实谬误,所以后来出现了一批改进的数据集,如 MMLU-Pro 和 MMLU-Redux。

劣势:

  • 复杂任务难以保证效果
    :自动评估基准通常在测试效果容易定义和评估的任务上表现良好 (如分类任务)。一旦任务比较复杂而且难以拆分为目标明确的子任务时,表现可能不及预期。
    例如:测试模型的 “数学能力” 任务。具体是算术、还是逻辑、亦或是推演新数学概念的能力?
    所以出现了一些无需拆分为子任务的
    通用性
    评估方式,由此评估出的模型整体表现就是评估目标的
    优良代理
  • 数据污染
    :网络上的数据一旦以纯文本的形式公开,那么由于数据爬虫,这些数据总归会出现在模型训练集中。所以在评估时很难保证模型真的没有见过测试集。


英文原文:
https://github.com/huggingface/evaluation-guidebook/blob/main/translations/zh/contents/automated-benchmarks/basics.md

原文作者: clefourrier

译者: SuSung-boy

审校: adeenayakup

大家好,我是汤师爷

最近几个月,Cursor迅速走红,成为一款强大的编程助手。Cursor不仅使用简单,而且通过集成各种大模型技术,编程能力一流。

Cursor是什么?

Cursor是一个类似VSCode的编辑器,集成了GPT-4、Claude 3.5等LLM模型。它本质上是在VSCode的基础上添加了AI辅助编程功能。

从界面布局到操作方式都与VSCode保持一致,包括扩展下载、Python环境配置、远程服务器连接和设置等功能。

如果你是VSCode的老用户,可以无缝切换到Cursor。

Cursor的下载

首先我们打开官网
cursor.com
,点击右上角的【download】按钮,下载 Cursor 的客户端。

配置中文插件

点击右上角的小图标,展开左侧界面,点击第四个插件图标,切换到插件栏。

搜索框中输入【中文】,找到中文简体插件,点击【Install】按钮。现在界面就是中文了!

用 Cursor 写出第一个程序

我们以制作一个俄罗斯方块游戏为例,手把手教你写一个完整的程序。

首先我们打开一个文件夹,点击【文件】,点击【打开】。

新建一个【cursor_tutorial】文件夹,用这个文件夹来存放代码文件。

我们点击右上角的按钮,打开侧边栏。调出对话框,并在对话框里发送:

用 Java 帮我写一个俄罗斯方块游戏。

过了大约十秒,就生成好代码了,点击【Run command】,cursor会自动启用终端执行命令:

javac Tetris.java Board.java Shape.java && java Tetris

俄罗斯方块游戏已经大功告成。

我们还可以继续为游戏加功能,在对话框里继续发送:

添加下一个方块预览

点击【Run command】。

下一个方块预览的功能已经实现好啦。

再添加一个分数系统的功能。

Cursor很快又完成了。

总结

看完这个小demo,我们可以看到Cursor很好上手,跟VSCode一样的操作方式,老VSCode用户直接无缝切换。

Java选手之前用的IDEA,可能一开始不习惯,但很快也能上手开发。

Cursor各种编程语言都能整,各种框架也都不在话下,真心建议试试Cursor,绝对是下一代编程工具。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,AI 编程、算法 Leetcode 详解、面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。