图文并茂重新认识下递归

大家好,我是若川。持续组织了5个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。

对于大部分前端(包括我)接触到递归的时候就是学函数的时候,对于递归的认知就是函数自己调用自己,然后给函数定义一个出口,通过这个函数将一件的事情拆分成同类型的小事情。但你会发现你写的递归情况很少,因为很多情况下递归是可以用循环去实现同样的效果,那对于递归具体使用场景,什么时候需要用递归,就是本文主要想探讨的话题。

递归只能去解决树形结构的问题吗?

对于很少使用递归来解决问题的很容易就会把递归想成只有树形情况才能使用递归,下面我们先通过解决树形情况深入了解递归能解决哪些场景的问题以及除了树形结构的数据,它还能处理什么问题?

「问题一」:获取根节点名称

点击当前节点获取根级节点的名称

这里用element的el-tree组件来做测试,根级节点不包括tree中的虚拟根节点,指的是数据的根节点

这就是虚拟根节点

f8cf16ada58bfc44dd522c39fa86c97e.png

实现的效果是这样的

e877b92f1de201927e8c26082c017da2.gif

这个需求很简单,我们很容易就想到了递归,为什么?

  • 获取根级:不停的获取当前节点的上级,至到没有上级就是根级

  • 递归出口条件:通过node.parent或者node.level

  • 递归中变化的状态值:node.parent

getRootNodeName(node) {if (!node) returnif (node.level === 1) {return node.label}return this.getRootNodeName(node.parent)
}

通过上面三个条件我们实现了这个递归函数,这里需要注意的是递归中变化的状态值,这个是整个递归中最难理解也最重要的部分,后文会展开来讲

这种递归其实很易就能写出来,循环就只能实现一样的效果

getRootNodeName(node) {if (!node) returnwhile (node.level > 1) {node = node.parent}return node.label
}

「问题二」:获取当前节点所在树的指定层级节点

这里有还有个约束条件,node节点中没有level属性了,只有parent属性指向节点的父节点,这里为了更好的表达,我们把node节点的中的虚拟根节点去除。

0808914849f82d384975d29030d1ef33.png

我们要实现这样的效果:点击当前节点获取当前节点所在的层级

f4e8bd5fa627c927e5ff91dd5840de59.gif

递归实现

getNodeLevel(node) {if (!node.parent) return 1return this.getNodeLevel(node.parent) + 1
}

当前这个递归函数主要实现思路就是在不停的递进,当node.parent不存在就说明当前节点是一级节点,然后在回溯的过程中在上级节点的层级 + 1 = 当前节点层级

31994f9c10c4bf3aeb35536995e8e553.png

递归:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开它。若干次之后,你打开面前的门后,发现只有一间屋子,没有门了。然后,你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你用这把钥匙打开了几扇门。

循环实现

getNodeLevel(node) {let n = 1let p = nodewhile (p.parent) {p = p.parentn++}return n
}

循环:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门(若前面两扇门都一样,那么这扇门和前两扇门也一样;如果第二扇门比第一扇门小,那么这扇门也比第二扇门小,你继续打开这扇门,一直这样继续下去直到打开所有的门。但是,入口处的人始终等不到你回去告诉他答案

上面两段话其实就很好的说出了递归和循环之间的差别,就是因为循环没有回溯的过程,我们当前的循环就不得不存储每个递归的状态值数据

「问题三」: 树节点的过滤

有人可能会问树节点的过滤这个在el-tree的用例中不就有吗?但是那个并不能解决我们实际场景的问题,举个例子来看下

<el-tree...:filter-node-method="filterNode"
/><script>
export default {methods: {filterNode(value, data) {if (!value) return truereturn data.label.indexOf(value) !== -1},}
}
</script>

存在的问题

ebf2f0f9281fd7cc8283089e0b1808f5.gif

当前的过滤只是通过label包含的方式把不符合的节点全都过滤掉了,而我们想要的效果应该是过滤不符合条件的分支,如果符合当前层级符合条件那它的下级就不应该过滤掉

递归实现

methods: {filterNodeMethod(value, data, node) {if (!value) {return true}return this.deepfilterChildNode(value, node)},deepfilterChildNode(value, node) {if (node.level === 1) return falseconst { filterNodeByIndexOf, deepfilterChildNode } = thisif (filterNodeByIndexOf(node.label, value)) {return true}return deepfilterChildNode(value, node.parent)},filterNodeByIndexOf(label, value) {return label.indexOf(value) !== -1}
}

循环实现

主要实现函数就是deepfilterChildNode, 接着再用循环来实现下同步的效果

methods: {deepfilterChildNode(value, node) {const { filterNodeByIndexOf } = thisif (filterNodeByIndexOf(node.label, value)) {return true}// 一级节点没有父级if (node.level === 1) return false// 往上找到最大那个节点结束const maxNode = 1 // 多级节点父级符合条件,不要、过滤子级let parentNode = node.parentwhile (parentNode.level > maxNode) {if (filterNodeByIndexOf(parentNode.label, value)) {return true}parentNode = parentNode.parent}// 当前节点的最大父节点都没找到return false},
}

「问题四」:实现instanceof

  • 作用:判断右边构造函数的原型对象是否存在于左边对象的原型链上

  • 原理:左侧的原型链中存在右侧的原型对象

左侧必须是对象

1 instaceof Number // false
Number(1) instaceof Number // false
new Number(1) instaceof Number // true

右侧必须是构造函数

let f1 = () => {};
let f2 = function () {}
class f3 {}
obj instanceof f1; // 报错
obj instanceof f2 // false
obj instanceof f3 // false

左侧的原型链中存在右侧的原型对象

let obj = {}
class ctor {}
console.log(obj instanceof ctor); // false
Object.setPrototypeOf(obj, new ctor)
console.log(obj instanceof ctor); // truelet getProto = Object.getPrototypeOf
getProto(getProto(obj)) === ctor.prototype // true

递归实现

function _instaceof(obj, ctor) {// 左边必须是对象if (!(obj && typeof obj === 'object')) {return false}// 获取对象的原型链对象let proto = Object.getPrototypeOf(obj)// 判断对象的原型链对象是否是构造函数函数的原型对象return proto === ctor.prototype || _instaceof(proto, ctor)
}

循环实现

function instanceOf1(obj, ctor) {if (!(obj && typeof obj === 'object')) {return false}let proto while(proto = Object.getPrototypeOf(obj)) {if (Object.is(proto, ctor.prototype)) return trueobj = proto}return false
}

「问题五」深度属性

测试数据

let obj = {a: {b: {c: 1,cc: {dd: {f: 'f Val'}}}},data: {v1: 'v1',v2: 'v2',v3: 'v3'}
}

获取属性

function getDeepObjAttr(obj, deepPath) {const deepGet = (o, paths) => {if (!paths.length) return oreturn deepGet(Reflect.get(o, paths.shift()), paths)}return deepGet(obj, deepPath.split('.'))
}
console.log(getDeepObjAttr(obj, 'a.b.cc.dd.f')) // f Val

设置属性

function setDeepObjAttr(model, deepPath, val) {let paths = deepPath.split('.')if (!paths.length) return modelconst setDeep = (o, p, i) => {if (i < 0) return olet prop = p[i]// 最后一层要设定的值if (i === p.length - 1) {Reflect.set(o, prop, val)} else {Reflect.set(o, prop, {...getDeepObjAttr(model, p.slice(0, i + 1).join('.')),...o})Reflect.deleteProperty(o, paths[i + 1])}return setDeep(o, p, --i)}return Object.assign(model,setDeep({}, [...paths], paths.length - 1))
}
setDeepObjAttr(obj, 'a.b.c', '2')
setDeepObjAttr(obj, 'a.b.cc.dd.f', 'f Val21')

改变后的obj

{"a": {"b": {"c": "2","cc": {"dd": {"f": "f Val21"}}}},"data": {"v1": "v11","v2": "v2","v3": "v3"}
}

这个递归有多个出口条件且并不是在单纯的做一件类型事情,且递归函数中引用的自由变量(全局变量)model,递归过程中如果修改model,其他递归中的变量也会受影响

对于递归那些状态值(变量)需要提取到全局,可以这样思考

类似于盗梦空间,我手里有10块钱,我做了第一个梦(n),在这个梦中我花了2块,接着又做了一个梦(n + 1), 在n+1中我还是有10块钱,前面梦中花掉的钱并不影响我这个梦中的钱。那这个状态值(变量)就是递归函数内部创建的局部变量,反之就需要把状态值(变量)放到全局

这里同样给出循环的实现

function getDeepObjAttr(obj, deepPath) {let paths = deepPath.split('.')if (!paths.length) return objlet prop,targetVal,tempObj = objwhile ((prop = paths.shift()) && paths.length) {if (!Reflect.has(tempObj, prop)) {return}tempObj = tempObj[prop]}return tempObj[prop]
}
function setDeepObjAttr(model, deepPath, val) {// 路径let paths = deepPath.split('.')// 目标值,存放符合路径下的所有属性let targetVal = {}// 用于查找每个对象的proplet pathsNew = paths.concat([])let propfor (let i = paths.length - 1, j = i; i >= 0; i--) {prop = paths[i]// 最后一层要设定的值if (i === j) {targetVal[prop] = val} else if (i === 0) {// 第一层需要直接替换第一个属性const obj = this.getDeepObjAttr(model, prop);Reflect.set(model, prop, Object.assign({}, obj, targetVal))} else {// 更新每一个层级的值(去除存起来的值)let curDeppObj = getDeepObjAttr(model, pathsNew.join('.'))// 将当前层级的值存储起来Reflect.set(targetVal, prop, Object.assign({}, curDeppObj, targetVal))// 删除上个路径存储的值Reflect.deleteProperty(targetVal, paths[i + 1])}// 将处理过的路径去除pathsNew.pop()}return model
}

对于递归是否只能解决树形结构的问题,还没有给出答案,又出现了一个问题,递归和循环的区别?

递归和循环有什么区别?

通过上面的五个例子我们发现递归和循环都能实现,其实递归与循环是两种不同的解决问题的典型思路, 都具有相同的特性,即做重复任务 。 单从算法设计上看,递归和循环并无优劣之别。然而,在实际开发中,因为函数调用的开销,递归常常会带来性能问题,特别是在求解规模不确定的情况下;而循环因为没有函数调用开销,所以效率会比递归高。

插一句,求解规模不确定还包含隐示的数据规模很大,但前端页面很少会碰到处理数据规模特别大的情况,所以用递归也没啥问题

相同点:

  • 将大事件拆分成小事情即**重复任务 **(循环、递归调用)

  • 处理过程中将状态值变换即状态展开

区别:

  • 如果使用循环我们就得建立堆栈来替代系统栈保存处理当前状态的内容(变量)

再次认识递归

let arr = [1, 2, 3, 4, 5, 6]function logArr(arr) {for (let i = 0; i < arr.length; i++) {console.log(arr[i]);}
}
logArr(arr)

让你依次打印输出中的每一项,这个能用递归实现吗???

先想想







答案是肯定可以

function logArr(arr) {const dfs = (idx) => {if (idx === arr.length) returnconsole.log(arr[idx]);dfs(idx + 1)}dfs(0)
}

斐波纳契数列

斐波纳契数列,指的是这样一个数列:1、1、2、3、5、8、13、21,简单来讲就是从第二个数之后的每个数是前两个数之和: F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2)

递归求解

function fibonacci(n) {// 递归终止条件if (n <= 2) {return 1}// 相同重复逻辑,缩小问题的规模return fibonacci(n - 1) + fibonacci(n - 2)
}
fibonacci(5)

状态展开生成树

77232692fd62b7ce21523906c5473695.png

这里面包含了许多重复计算,计算fib(5)时

1次fib(4)
2次fib(3)
3次fib(2)
5次fib(1) 
3次fib(0)

像这种重复的计算就相当于相同的分支,在dfs(深度优先搜索)中有个很重要的操作叫做剪枝; 在当前递归的实现中那些重复的计算我们可以进行缓存当后面出现重复计算就不再调用,也算是剪枝,这对于递归是很大的性能优化。

// 创建一个空对象,作为缓存的容器
let cache = {};
function fibonacci(n) {if (n === 1 || n === 2) {return 1;}if (cache[n]) {return cache[n];}return cache[n] = fibonacci(n - 1) + fibonacci(n - 2);
}

还有一种缓存的方案,在递归的过程中进行计算,回溯的时候把最后的结果返回

function fibonacci(n, cur = 0, next = 1) {if(n == 0) return 0;if(n == 1) return next; return fibonacci(n - 1, next, cur + next);
}

循环求解

function fibonacci(n) {if (n <= 2) return 1// 维护"栈"的状态值,以便状态回溯let first = 1// 维护"栈"的状态值,以便状态回溯let second = 1let ret = 0n -= 2while (n--) {// 当前数 = 前一个数 + 前两个数ret = first + secondfirst = secondsecond = ret}return ret
}

现在可以回答第一个问题了,递归不是只能去解决树形数据结构和明显的上下级关系的数据,对这种情况的数据是非常符合递归的定义所以我们很容易能想像出来,像斐波纳契数列 、 阶乘 、杨辉三角..., 这种通过递推可以求解的方式也可以用递归,通过递归过程中变化状态值来求解答案。

二分查找

循环实现

function binarySearch(nums, target) {let l = 0let r = nums.length - 1let midwhile (l <= r) {mid = (l + r) >> 1if (nums[mid] === target) {return mid} else if (nums[mid] > target) {r = mid - 1} else {l = mid + 1}}return -1
}

递归实现

变换状态值,状态展开生成树

function binarySearch(nums, target, l = 0, r = nums.length - 1) {let midwhile (l <= r) {mid = (l + r) >> 1if (nums[mid] === target) {return mid} else if (nums[mid] > target) {return binarySearch(nums, target, l, mid - 1)} else {return binarySearch(nums, target, mid + 1, r)}}return -1
}

算法中递归的应用

在算法中对于递归的应用场景特别特别多,dfs:回溯、二叉树遍历、翻转、路径总和...,把以上这些类型的题用递归刷几题,对于递归就会有更深的认知和理解,后面碰到的需求如果可以用递归解,自然你就能想到递归。

现在给出一些递归可解的leetcode题,并不是说去学算法刷题,这里的目的是做这些题可以让我们对于用递归有更深入的了解,递归用的不太多的可能需要花点时间,全都掌握之后对自己也是个提升

二叉树遍历

二叉树有三种遍历方式:

  • 前序遍历:根、左、右

  • 中序遍历:左、根、右

  • 后续遍历:左、右、根

前序遍历

leetcode144. 二叉树的前序遍历

/*** @param {TreeNode} root* @return {number[]}*/
var preorderTraversal = function(root) {let ans = []const preIterator = (root) => {if (!root) returnans.push(root.val)preIterator(root.left)preIterator(root.right)}preIterator(root)return ans
}

中序遍历

leetcode94. 二叉树的中序遍历

/*** @param {TreeNode} root* @return {number[]}*/
var inorderTraversal = function(root) {let ans = []const inIterator = (root) => {if (!root) returninIterator(root.left)ans.push(root.val)inIterator(root.right)}inIterator(root)return ans
};

后序遍历

leetcode145. 二叉树的后序遍历

/*** @param {TreeNode} root* @return {number[]}*/
var postorderTraversal = function(root) {let ans = []const postIterator = (root) => {if (!root) returnif (!postIterator(root.left)) {if (!postIterator(root.right)) {ans.push(root.val)}}}postIterator(root)return ans
};

路径总和

leetcode112. 路径总和

var hasPathSum = function (root, targetSum) {if (!root) return falsetargetSum = targetSum - root.val// 当前节点是叶子节点if (!root.left && !root.right) {// 如果当前节点刚好能符合所有ragetSum, 表示当前节点就是目标节点return targetSum === 0}return hasPathSum(root.left, targetSum) || hasPathSum(root.right, targetSum)
};

leetcode113. 路径总和 II

/*** @param {TreeNode} root* @param {number} targetSum* @return {number[][]}*/
var pathSum = function (root, targetSum) {let ans = []const dfs = (root, targetSum, temp = []) => {if (!root) returntemp.push(root.val)targetSum -= root.valif (!root.left && !root.right && targetSum === 0) {ans.push([...temp])}dfs(root.left, targetSum, temp)dfs(root.right, targetSum, temp)temp.pop()}dfs(root, targetSum)return ans
};

回溯

leetcode39. 组合总和

/*** @param {number[]} candidates* @param {number} target* @return {number[][]}*/var combinationSum = function (candidates, target) {const ans = []const dfs = (idx, target, temp = []) => {if (idx === candidates.length) returnif (target < 0) returnif (target === 0) {ans.push([...temp])temp = []return}temp.push(candidates[idx])dfs(idx, target - candidates[idx], temp)temp.pop()dfs(idx + 1, target, temp)}dfs(0, target)return ans
};

写在最后

业精于勤,荒于嬉

如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下,我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。

eb4db1db0858580e4e6f20c39fc90f08.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

4216348d9b92d17f6710a8c0ff8108d1.png

识别方二维码加我微信、拉你进源码共读

今日话题

略。分享、收藏、点赞、在看我的文章就是对我最大的支持~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/275041.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

unity 全息交互ui_UI向3D投影全息界面的连续发展

unity 全息交互uiThe user interface has been natural in its evolution and strategically heading towards the 3D-projection holographic interface (3D-PHI) era.用户界面在其发展过程中一直很自然&#xff0c;并且在战略上正朝着3D投影全息界面( 3D-PHI )时代迈进。 Si…

前端构建新世代,Esbuild 原来还能这么玩!

大家好&#xff0c;我是若川。持续组织了5个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。今天分享一篇esbui…

平面设计师和ui设计师_平面设计师为什么要享受所有乐趣?

平面设计师和ui设计师Graphic designers are pretty cool. We have to admit that. Be it their dressing style, their attitude and most importantly their enviable gadgets. Large Mac monitor, wacom tablet, drawing sets, swatchbooks , iPad pro with pencil, humungo…

web表单设计:点石成金_设计复杂的用户表单:12个UX最佳实践

web表单设计:点石成金It’s been a few years that I’ve been taking interest in designing complex user forms, where a lot of information is requested from users. Here are a few industries where you regularly find such flows:几年来&#xff0c;我一直对设计复杂…

跨平台开发框架到底哪家强?5款主流框架横向对比!

跨平台开发框架到底哪家强&#xff1f;目前市场上有多个专业做跨平台开发的框架&#xff0c;那么对开发者来说究竟哪一个框架更符合自己的需求呢&#xff1f;笔者特地总结对比了一下不同框架的特性。国内外笔者选择了一共5个主流的测评对象&#xff0c;分别是RN&#xff0c;Flu…

c#创建web应用程序_创建Web应用程序图标集的6个步骤

c#创建web应用程序I am not great at creating logos or icons, mainly because of the lack of practice. So when I was tasked to create an unique icon set for our web app, I wasn’t confident that things will turn out right. After researching effective and rele…

基于pnpm + lerna + typescript的最佳项目实践 - 理论篇

本文来自作者金虹桥程序员 投稿原文链接&#xff1a;https://juejin.cn/post/7043998041786810398本系列文章分为两篇&#xff1a;理论篇和实践篇 理论篇&#xff1a;介绍pnpm&#xff08;pnpm的特点、解决的问题等&#xff09;、lerna&#xff08;lerna的常用命令&#xff09;…

nginx 多进程 + io多路复用 实现高并发

一、nginx 高并发原理 简单介绍&#xff1a;nginx 采用的是多进程&#xff08;单线程&#xff09; io多路复用(epoll)模型 实现高并发 二、nginx 多进程 启动nginx解析初始化配置文件后会 创建&#xff08;fork&#xff09;一个master进程 之后 这个进程会退出 master 进程会…

ux设计工具_UX设计中的工具和实用主义

ux设计工具There’s a zillion tools for User Experience and User Interface Design. Don’t take my word for it: a simple Google search for “what are the best tools for wireframing” (to take just one aspect of UX) leads you to endless pages of “The 20 best…

幕后常驻嘉宾配音小姐姐的2021年度总结

大家好&#xff0c;我是若川。这是公众号幕后常驻嘉宾配音小姐姐&#xff0c;看完了上一个阿源小姐姐的年度总结《一张图看程序媛阿源的2021个人年度流水账》&#xff0c;写的年度总结投稿。点击以下音频可以查看收听往期更多音频。以下是正文~Hi&#xff0c;大家好呀~我是若川…

结果规格化_结果

结果规格化If you’ve seen an Instagram story involving a question and people tilting their heads, you probably were looking at the “Who Is More” Instagram filter. In this article, I will share the creative process and decision making behind this filter.如…

2021 年 JavaScript 大事记

大家好&#xff0c;我是 ConardLi&#xff0c;不知不觉中&#xff0c;2021 年已经接近尾声了&#xff0c;不知道在 2021 这一年&#xff0c;你收获了什么&#xff1f;又失去了什么呢&#xff1f;又到了开始做年终总结的时候了&#xff0c;今天&#xff0c;我来给 JavaScript 做…

动画 制作_您希望制作的10个醒目的徽标动画

动画 制作重点 (Top highlight)标志设计 (Logo Design) Have you ever watched paint dry? No? I didn’t think so. How about watched a turtle crossing the road? Probably not. Maybe spent an hour standing in line at the post office? Well that’s pretty likely…

使用 CSS 用户选择控制选择

IE10 平台预览 4 包括一个新的 CSS 属性的支持-ms-user-select&#xff0c;这使得 Web 开发者控制完全可以选择什么的文本&#xff0c;在其网站上更容易。如果你是看我一整天都在我的工作站&#xff0c;您会注意到我读计算机上时&#xff0c;我选择的文本。我不是只有一个人读起…

一个在校的普通前端小姐姐的2021

大家好&#xff0c;我是若川。这是我的源码共读群里一个大三的前端小姐姐&#xff08;小曹同学&#xff09;的年度总结。她写了5篇源码笔记。同时做了很多项目&#xff0c;获得了很多奖。而且策划和建立了学校工作室的前端训练营&#xff0c;40人报名参加。总之就是现在的大学生…

按钮 交互_SwiftUI中的微交互—菜单按钮动画

按钮 交互Microinteractions have become increasingly important in a world with a dizzying number of digital platforms and an ocean of content. While microinteractions used to be considered an interesting resource in the early days of digital design, in toda…

选择控件— UI组件系列

重点 (Top highlight)The word “toggle” is a reference to a switch with a short handle that alternates between two states each time it is activated. You encounter it every time you “switch” on the lights.单词“ toggle”是指带有短手柄的开关&#xff0c;该开…

SEE Conf: Umi 4 设计思路文字稿

大家好&#xff0c;我是若川。持续组织了5个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。复制此链接 https:…

用户体验改善案例_改善用户体验研究的5种习惯

用户体验改善案例There’s plenty of misunderstanding around user research, whether it’s the concept of validation or one-off anecdotes being thrown around as concrete evidence for a product decision.用户研究存在很多误解&#xff0c;无论是验证的概念还是一次性…

巴克莱对冲_“巴克莱的财政预算案”:使金钱管理对心理健康有效—用户体验案例研究

巴克莱对冲Disclaimer: all official Barclays assets used for this project are purely for educational/project purposes only and do not reflect the intentions of Barclays or any of its affiliates.免责声明&#xff1a;用于此项目的所有官方巴克莱资产纯粹是出于教育…